""" Armor Selection Dialog for Lemontropia Suite Choose between full API armor sets or build custom sets from individual pieces """ import json from decimal import Decimal from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QLineEdit, QDialogButtonBox, QGroupBox, QFormLayout, QTabWidget, QWidget, QMessageBox, QComboBox ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QColor from typing import Optional, Dict, Any, List from core.nexus_full_api import get_nexus_api, NexusArmorSet, NexusArmor from core.armor_system import ArmorSlot, ArmorPiece, ProtectionProfile class ArmorSelectionDialog(QDialog): """ Dialog for selecting armor - either full sets from API or custom pieces. """ # Signal emits: (mode, data) where mode is 'set' or 'custom' # For 'set': data is {'name': str, 'pieces': List[ArmorPiece], 'total_protection': ProtectionProfile, 'total_decay_per_hit': Decimal} # For 'custom': data is {'pieces': Dict[ArmorSlot, ArmorPiece]} armor_selected = pyqtSignal(str, dict) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Select Armor - Lemontropia Suite") self.setMinimumSize(1000, 700) self.api = get_nexus_api() self.all_armor_sets: List[NexusArmorSet] = [] self.all_armors: List[NexusArmor] = [] self._setup_ui() self._load_data() def _setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) # Header header = QLabel("🛡️ Select Your Armor") header.setStyleSheet("font-size: 18px; font-weight: bold; color: #FFD700;") layout.addWidget(header) # Tabs for Full Sets vs Custom self.tabs = QTabWidget() # === FULL SETS TAB === self.tab_sets = self._create_sets_tab() self.tabs.addTab(self.tab_sets, "⚔️ Full Armor Sets") # === CUSTOM SET TAB === self.tab_custom = self._create_custom_tab() self.tabs.addTab(self.tab_custom, "🔧 Custom Set") layout.addWidget(self.tabs) # Preview panel (shared) self.preview_group = QGroupBox("Armor Preview") preview_layout = QFormLayout(self.preview_group) self.preview_name = QLabel("-") self.preview_type = QLabel("-") self.preview_protection = QLabel("-") self.preview_decay = QLabel("-") self.preview_pieces = QLabel("-") preview_layout.addRow("Name:", self.preview_name) preview_layout.addRow("Type:", self.preview_type) preview_layout.addRow("Protection:", self.preview_protection) preview_layout.addRow("Decay/Hit:", self.preview_decay) preview_layout.addRow("Pieces:", self.preview_pieces) layout.addWidget(self.preview_group) # Buttons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) self.ok_button.setEnabled(False) layout.addWidget(buttons) def _create_sets_tab(self) -> QWidget: """Create the Full Sets tab.""" widget = QWidget() layout = QVBoxLayout(widget) # Search search_layout = QHBoxLayout() search_layout.addWidget(QLabel("Search:")) self.set_search = QLineEdit() self.set_search.setPlaceholderText("Search armor sets (e.g., 'Ghost', 'Shogun', 'Frontier')...") self.set_search.textChanged.connect(self._filter_sets) search_layout.addWidget(self.set_search) clear_btn = QPushButton("Clear") clear_btn.clicked.connect(self.set_search.clear) search_layout.addWidget(clear_btn) layout.addLayout(search_layout) # Sets tree self.sets_tree = QTreeWidget() self.sets_tree.setHeaderLabels([ "Set Name", "Pieces", "Impact", "Cut", "Stab", "Burn", "Cold", "Acid", "Electric", "Total", "Decay/Hit" ]) header = self.sets_tree.header() header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.sets_tree.itemSelectionChanged.connect(self._on_set_selection_changed) self.sets_tree.itemDoubleClicked.connect(self._on_accept) layout.addWidget(self.sets_tree) return widget def _create_custom_tab(self) -> QWidget: """Create the Custom Set tab.""" widget = QWidget() layout = QVBoxLayout(widget) # Instructions instructions = QLabel( "Build a custom armor set by selecting individual pieces for each slot.\n" "Click on each slot to search and select armor pieces from the API." ) instructions.setStyleSheet("color: #888888; padding: 10px;") layout.addWidget(instructions) # Custom pieces container custom_layout = QHBoxLayout() # Slot selectors self.custom_slots: Dict[ArmorSlot, QComboBox] = {} slot_layout = QVBoxLayout() for slot in ArmorSlot: row = QHBoxLayout() label = QLabel(f"{slot.value.title()}:") label.setFixedWidth(80) row.addWidget(label) combo = QComboBox() combo.setMinimumWidth(250) combo.addItem(f"-- Select {slot.value.title()} --") combo.currentIndexChanged.connect(lambda idx, s=slot: self._on_custom_slot_changed(s)) self.custom_slots[slot] = combo row.addWidget(combo) search_btn = QPushButton("🔍") search_btn.setFixedWidth(40) search_btn.setToolTip(f"Search {slot.value.title()} pieces") search_btn.clicked.connect(lambda checked, s=slot: self._search_custom_piece(s)) row.addWidget(search_btn) slot_layout.addLayout(row) custom_layout.addLayout(slot_layout) custom_layout.addStretch() layout.addLayout(custom_layout) # Summary self.custom_summary = QLabel("No pieces selected") self.custom_summary.setStyleSheet("color: #4caf50; font-weight: bold; padding: 10px;") layout.addWidget(self.custom_summary) return widget def _load_data(self): """Load armor data from API.""" # Load armor sets self.all_armor_sets = self.api.get_all_armor_sets() self._populate_sets(self.all_armor_sets) # Load individual armors for custom set self.all_armors = self.api.get_all_armors() self._populate_custom_slots() def _populate_sets(self, sets: List[NexusArmorSet]): """Populate the armor sets tree.""" self.sets_tree.clear() # Sort by total protection sets = sorted(sets, key=lambda s: s.total_protection.get_total(), reverse=True) for armor_set in sets: item = QTreeWidgetItem() item.setText(0, armor_set.name) item.setText(1, str(len(armor_set.pieces))) 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)) item.setText(9, str(prot.get_total())) # Calculate decay per hit (using durability from set if available) # Default durability 20000 = 0.05 * (1 - 20000/100000) = 0.04 PEC per HP decay_per_hit = self._calculate_set_decay(armor_set) item.setText(10, f"{decay_per_hit:.4f}") # Color code by protection level total_prot = prot.get_total() if total_prot >= 100: item.setForeground(0, QColor("#FFD700")) # Gold for high protection elif total_prot >= 50: item.setForeground(0, QColor("#C0C0C0")) # Silver for medium item.setData(0, Qt.ItemDataRole.UserRole, armor_set) self.sets_tree.addTopLevelItem(item) def _calculate_set_decay(self, armor_set: NexusArmorSet) -> Decimal: """Calculate estimated decay per hit for an armor set.""" # Get durability from set properties or use default durability = 20000 # Default # Calculate decay per HP: 0.05 * (1 - durability/100000) decay_per_hp = Decimal("0.05") * (Decimal(1) - Decimal(durability) / Decimal("100000")) # Estimate decay per hit (assume 10 HP absorbed per hit on average) typical_hit = Decimal("10") return decay_per_hp * typical_hit / Decimal("100") # Convert to PED def _populate_custom_slots(self): """Populate custom slot dropdowns with armor pieces.""" # Group armors by slot type based on name slot_keywords = { ArmorSlot.HEAD: ['helmet', 'cap'], ArmorSlot.TORSO: ['harness', 'chest', 'torso', 'vest'], ArmorSlot.ARMS: ['arm', 'shoulder'], ArmorSlot.HANDS: ['glove', 'hand'], ArmorSlot.LEGS: ['thigh', 'leg'], ArmorSlot.SHINS: ['shin'], ArmorSlot.FEET: ['foot', 'boot'], } for slot, keywords in slot_keywords.items(): combo = self.custom_slots[slot] combo.clear() combo.addItem(f"-- Select {slot.value.title()} --") # Find matching armors matching = [] for armor in self.all_armors: name_lower = armor.name.lower() if any(kw in name_lower for kw in keywords): matching.append(armor) # Sort by name matching.sort(key=lambda a: a.name) for armor in matching: # Calculate protection total total_prot = (armor.protection_impact + armor.protection_cut + armor.protection_stab + armor.protection_burn) display = f"{armor.name} (Prot: {total_prot})" combo.addItem(display, armor) def _filter_sets(self): """Filter armor sets based on search text.""" search = self.set_search.text().lower() if not search: self._populate_sets(self.all_armor_sets) return filtered = [s for s in self.all_armor_sets if search in s.name.lower()] self._populate_sets(filtered) def _on_set_selection_changed(self): """Handle selection change in sets tree.""" items = self.sets_tree.selectedItems() if not items: self.ok_button.setEnabled(False) self._clear_preview() return armor_set = items[0].data(0, Qt.ItemDataRole.UserRole) self._update_set_preview(armor_set) self.ok_button.setEnabled(True) 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)") prot = armor_set.total_protection prot_str = f"Impact: {prot.impact}, Cut: {prot.cut}, Stab: {prot.stab}\n" prot_str += f"Burn: {prot.burn}, Cold: {prot.cold}, Acid: {prot.acid}, Elec: {prot.electric}\n" prot_str += f"Total: {prot.get_total()}" self.preview_protection.setText(prot_str) decay = self._calculate_set_decay(armor_set) self.preview_decay.setText(f"{decay:.4f} PED per hit") pieces_str = "\n".join([f" • {p}" for p in armor_set.pieces]) self.preview_pieces.setText(pieces_str) def _on_custom_slot_changed(self, slot: ArmorSlot): """Handle change in custom slot selection.""" self._update_custom_summary() # Enable OK if at least one piece selected has_selection = any( combo.currentIndex() > 0 for combo in self.custom_slots.values() ) self.ok_button.setEnabled(has_selection) def _search_custom_piece(self, slot: ArmorSlot): """Open search dialog for custom piece.""" # TODO: Implement piece search dialog QMessageBox.information(self, "Search", f"Search for {slot.value.title()} piece") def _update_custom_summary(self): """Update custom set summary.""" selected = [] total_protection = ProtectionProfile() for slot, combo in self.custom_slots.items(): if combo.currentIndex() > 0: armor = combo.currentData() if armor: selected.append(f"{slot.value}: {armor.name}") total_protection = total_protection.add(ProtectionProfile( impact=armor.protection_impact, cut=armor.protection_cut, stab=armor.protection_stab, burn=armor.protection_burn, cold=armor.protection_cold, acid=armor.protection_acid, electric=armor.protection_electric, )) if selected: self.custom_summary.setText( f"Selected {len(selected)} pieces\n" f"Total Protection: {total_protection.get_total()}\n" + "\n".join(selected) ) else: self.custom_summary.setText("No pieces selected") def _on_accept(self): """Handle OK button.""" current_tab = self.tabs.currentIndex() if current_tab == 0: # Full Sets tab items = self.sets_tree.selectedItems() if items: armor_set = items[0].data(0, Qt.ItemDataRole.UserRole) # Create ArmorPieces from set pieces = self._create_pieces_from_set(armor_set) data = { 'name': armor_set.name, 'pieces': pieces, 'total_protection': armor_set.total_protection, 'total_decay_per_hit': self._calculate_set_decay(armor_set), } self.armor_selected.emit('set', data) self.accept() else: # Custom Set tab pieces = {} for slot, combo in self.custom_slots.items(): if combo.currentIndex() > 0: armor = combo.currentData() if armor: piece = self._create_piece_from_api(armor, slot) pieces[slot] = piece if pieces: data = {'pieces': pieces} self.armor_selected.emit('custom', data) self.accept() def _create_pieces_from_set(self, armor_set: NexusArmorSet) -> List[ArmorPiece]: """Create ArmorPiece objects from an armor set.""" pieces = [] # Map slot names slot_mapping = { 'head': ArmorSlot.HEAD, 'helmet': ArmorSlot.HEAD, 'cap': ArmorSlot.HEAD, 'torso': ArmorSlot.TORSO, 'harness': ArmorSlot.TORSO, 'chest': ArmorSlot.TORSO, 'arms': ArmorSlot.ARMS, 'arm': ArmorSlot.ARMS, 'hands': ArmorSlot.HANDS, 'gloves': ArmorSlot.HANDS, 'legs': ArmorSlot.LEGS, 'thigh': ArmorSlot.LEGS, 'shins': ArmorSlot.SHINS, 'shin': ArmorSlot.SHINS, 'feet': ArmorSlot.FEET, 'foot': ArmorSlot.FEET, 'boots': ArmorSlot.FEET, } # Find matching armor pieces for piece_name in armor_set.pieces: # Find in API results matching = None for armor in self.all_armors: if armor.name == piece_name: matching = armor break if matching: # Determine slot from name slot = ArmorSlot.TORSO # Default name_lower = piece_name.lower() for keyword, mapped_slot in slot_mapping.items(): if keyword in name_lower: slot = mapped_slot break piece = self._create_piece_from_api(matching, slot) # Set protection from set total (API individual pieces have 0) piece.protection = armor_set.total_protection pieces.append(piece) return pieces def _create_piece_from_api(self, armor: NexusArmor, slot: ArmorSlot) -> ArmorPiece: """Create ArmorPiece from NexusArmor API data.""" durability = armor.durability or 20000 decay_per_hp = Decimal("0.05") * (Decimal(1) - Decimal(durability) / Decimal("100000")) return ArmorPiece( item_id=armor.item_id, name=armor.name, slot=slot, protection=ProtectionProfile( impact=armor.protection_impact, cut=armor.protection_cut, stab=armor.protection_stab, burn=armor.protection_burn, cold=armor.protection_cold, acid=armor.protection_acid, electric=armor.protection_electric, ), durability=durability, decay_per_hp=decay_per_hp, ) def _clear_preview(self): """Clear preview panel.""" self.preview_name.setText("-") self.preview_type.setText("-") self.preview_protection.setText("-") self.preview_decay.setText("-") self.preview_pieces.setText("-") # Main entry for testing if __name__ == "__main__": import sys import logging from PyQt6.QtWidgets import QApplication logging.basicConfig(level=logging.INFO) app = QApplication(sys.argv) app.setStyle('Fusion') dialog = ArmorSelectionDialog() # Connect signal for testing dialog.armor_selected.connect( lambda mode, data: print(f"Selected mode: {mode}, data: {data}") ) if dialog.exec() == QDialog.DialogCode.Accepted: print("Armor selected!") sys.exit(0)