diff --git a/ui/armor_selection_dialog.py b/ui/armor_selection_dialog.py new file mode 100644 index 0000000..8146c1f --- /dev/null +++ b/ui/armor_selection_dialog.py @@ -0,0 +1,494 @@ +""" +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) diff --git a/ui/loadout_manager.py b/ui/loadout_manager.py index b65dc26..830148e 100644 --- a/ui/loadout_manager.py +++ b/ui/loadout_manager.py @@ -213,11 +213,15 @@ class LoadoutConfig: def get_armor_decay_per_hit(self) -> Decimal: """Calculate armor decay cost per hit taken (in PED).""" + # Use new armor system if available + if self.current_armor_decay > 0: + return self.current_armor_decay + + # Legacy fallback decay_per_hit = Decimal("0") if self.equipped_armor: decay_per_hit = self.equipped_armor.get_total_decay_per_hit() else: - # Legacy fallback decay_per_hit = self.armor_decay_pec # Add plate decay costs @@ -282,9 +286,13 @@ class LoadoutConfig: def get_total_protection(self) -> ProtectionProfile: """Get total protection from equipped armor.""" + # Use new armor system if available + if self.current_armor_protection.get_total() > 0: + return self.current_armor_protection + + # Legacy fallback if self.equipped_armor: return self.equipped_armor.get_total_protection() - # Legacy fallback return ProtectionProfile( stab=self.protection_stab, cut=self.protection_cut, @@ -1082,8 +1090,17 @@ class LoadoutManagerDialog(QDialog): self.current_loadout: Optional[LoadoutConfig] = None self.current_weapon: Optional[WeaponStats] = None + + # New armor system - API-based + self.current_armor_set_name: str = "None" + self.current_armor_pieces: List[ArmorPiece] = [] + self.current_armor_protection: ProtectionProfile = ProtectionProfile() + self.current_armor_decay: Decimal = Decimal("0") + + # Legacy (to be removed) 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 @@ -1639,197 +1656,65 @@ class LoadoutManagerDialog(QDialog): 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) + """Open new armor selection dialog (API-based sets or custom pieces).""" + from ui.armor_selection_dialog import ArmorSelectionDialog + dialog = ArmorSelectionDialog(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 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 with full data - api = get_nexus_api() - all_armors = api.get_all_armors() - - # Map slot names to armor slot widgets - slot_mapping = { - 'head': self.slot_widgets[ArmorSlot.HEAD], - 'torso': self.slot_widgets[ArmorSlot.TORSO], - 'harness': self.slot_widgets[ArmorSlot.TORSO], - 'chest': self.slot_widgets[ArmorSlot.TORSO], - 'arms': self.slot_widgets[ArmorSlot.ARMS], - 'arm guards': self.slot_widgets[ArmorSlot.ARMS], - 'armguards': self.slot_widgets[ArmorSlot.ARMS], - 'hands': self.slot_widgets[ArmorSlot.HANDS], - 'gloves': self.slot_widgets[ArmorSlot.HANDS], - 'legs': self.slot_widgets[ArmorSlot.LEGS], - 'thigh guards': self.slot_widgets[ArmorSlot.LEGS], - 'thighguards': self.slot_widgets[ArmorSlot.LEGS], - 'shins': self.slot_widgets[ArmorSlot.SHINS], - 'shin guards': self.slot_widgets[ArmorSlot.SHINS], - 'shinguards': self.slot_widgets[ArmorSlot.SHINS], - 'feet': self.slot_widgets[ArmorSlot.FEET], - 'foot guards': self.slot_widgets[ArmorSlot.FEET], - 'footguards': self.slot_widgets[ArmorSlot.FEET], - } - - pieces_found = 0 - pieces_not_found = [] - - # Store the full set for protection calculations - self.current_armor_set = armor_set - - # 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 + def _on_api_armor_selected(self, mode: str, data: dict): + """Handle armor selection from new dialog.""" + if mode == 'set': + # Full armor set selected + self.current_armor_set_name = data['name'] + self.current_armor_pieces = data['pieces'] + self.current_armor_protection = data['total_protection'] + self.current_armor_decay = data['total_decay_per_hit'] - if matching_armor: - # Get slot from the armor's Type field or parse from name - armor_type = matching_armor.type.lower() - slot_widget = None - - # First try direct Type match - if armor_type in slot_mapping: - slot_widget = slot_mapping[armor_type] - else: - # Try parsing from armor type - for type_key, widget in slot_mapping.items(): - if type_key in armor_type: - slot_widget = widget - break - - # If still no match, try parsing from name - if not slot_widget: - name_lower = piece_name.lower() - if 'helmet' in name_lower or 'cap' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.HEAD] - elif 'harness' in name_lower or 'chest' in name_lower or 'torso' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.TORSO] - elif 'arm' in name_lower or 'shoulder' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.ARMS] - elif 'glove' in name_lower or 'hand' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.HANDS] - elif 'thigh' in name_lower or ('leg' in name_lower and 'shin' not in name_lower): - slot_widget = self.slot_widgets[ArmorSlot.LEGS] - elif 'shin' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.SHINS] - elif 'foot' in name_lower or 'boot' in name_lower: - slot_widget = self.slot_widgets[ArmorSlot.FEET] - - if slot_widget: - # Create ArmorPiece from NexusArmor - # Use the SET'S protection values, not individual piece (which are 0 in API) - 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) if armor_type in slot_mapping else self._get_slot_from_name(piece_name), - # Use armor set's total protection for each piece - # In EU, protection comes from the full set, not individual pieces - protection=ProtectionProfile( - impact=armor_set.total_protection.impact, - cut=armor_set.total_protection.cut, - stab=armor_set.total_protection.stab, - burn=armor_set.total_protection.burn, - cold=armor_set.total_protection.cold, - acid=armor_set.total_protection.acid, - electric=armor_set.total_protection.electric, - ), - durability=matching_armor.durability, - decay_per_hp=Decimal("0.05") * (Decimal(1) - Decimal(matching_armor.durability) / Decimal("100000")) - ) - slot_widget.set_piece(piece) - pieces_found += 1 - else: - pieces_not_found.append(f"{piece_name} (unknown slot: {armor_type})") - else: - pieces_not_found.append(piece_name) + # Update UI + self.armor_set_label.setText(f"āœ“ {data['name']}") + self.protection_summary_label.setText( + f"Total: {data['total_protection'].get_total():.1f} | Decay: {data['total_decay_per_hit']:.4f}/hit" + ) + + QMessageBox.information( + self, + "Armor Set Equipped", + f"Equipped: {data['name']}\n" + f"Protection: {data['total_protection'].get_total():.1f}\n" + f"Decay/Hit: {data['total_decay_per_hit']:.4f} PED" + ) + else: + # Custom pieces selected + pieces = data['pieces'] + self.current_armor_set_name = "Custom Set" + self.current_armor_pieces = list(pieces.values()) + + # Calculate total protection + total_prot = ProtectionProfile() + for piece in pieces.values(): + total_prot = total_prot.add(piece.protection) + self.current_armor_protection = total_prot + + # Calculate total decay + total_decay = sum(p.decay_per_hp * Decimal("10") for p in pieces.values()) / Decimal("100") + self.current_armor_decay = total_decay + + # Update UI + self.armor_set_label.setText(f"āœ“ Custom ({len(pieces)} pieces)") + self.protection_summary_label.setText( + f"Total: {total_prot.get_total():.1f} | Decay: {total_decay:.4f}/hit" + ) + + QMessageBox.information( + self, + "Custom Armor Equipped", + f"Equipped {len(pieces)} pieces:\n" + + "\n".join([f" • {p.name}" for p in pieces.values()]) + ) - # 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}" - - # DEBUG: Show protection values being assigned - msg += f"\n\nšŸ“Š Debug Info:\n" - msg += f"Set Protection: Impact={armor_set.total_protection.impact}, Cut={armor_set.total_protection.cut}, Stab={armor_set.total_protection.stab}\n" - msg += f"Each piece assigned: {armor_set.total_protection.get_total()} total protection\n" - - # Check first equipped piece - first_slot = list(self.slot_widgets.values())[0] - first_piece = first_slot.get_piece() - if first_piece: - msg += f"First piece ({first_piece.name}): prot={first_piece.protection.get_total()}" - - 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 or 'chest' 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 _get_slot_from_name(self, piece_name: str) -> 'ArmorSlot': - """Map armor piece name to ArmorSlot enum.""" - name_lower = piece_name.lower() - if 'helmet' in name_lower or 'cap' in name_lower: - return ArmorSlot.HEAD - elif 'harness' in name_lower or 'chest' in name_lower or 'torso' in name_lower: - return ArmorSlot.TORSO - elif 'arm' in name_lower or 'shoulder' in name_lower: - return ArmorSlot.ARMS - elif 'glove' in name_lower or 'hand' in name_lower: - return ArmorSlot.HANDS - elif 'thigh' in name_lower or ('leg' in name_lower and 'shin' not in name_lower): - return ArmorSlot.LEGS - elif 'shin' in name_lower: - return ArmorSlot.SHINS - elif 'foot' in name_lower or 'boot' in name_lower: - 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 @@ -2130,13 +2015,12 @@ class LoadoutManagerDialog(QDialog): config = self._get_current_config() # DEBUG: Log armor status - if config.equipped_armor: - pieces = config.equipped_armor.get_all_pieces() - logger.debug(f"_update_calculations: {len(pieces)} pieces equipped") - for slot, piece in pieces.items(): - logger.debug(f" {slot}: {piece.name}, prot={piece.protection.get_total()}") + if self.current_armor_pieces: + logger.debug(f"_update_calculations: {len(self.current_armor_pieces)} armor pieces") + logger.debug(f" Total protection: {self.current_armor_protection.get_total()}") + logger.debug(f" Decay per hit: {self.current_armor_decay}") else: - logger.debug("_update_calculations: No equipped_armor") + logger.debug("_update_calculations: No armor pieces") # Weapon metrics (per shot, not per hour) cost_per_shot_pec = config.get_total_decay_per_shot() + config.get_total_ammo_per_shot() @@ -2151,20 +2035,24 @@ class LoadoutManagerDialog(QDialog): dpp = config.calculate_dpp() self.dpp_label.setText(f"{dpp:.4f}") - # Armor metrics (cost per hit) - cost_per_hit = config.get_armor_decay_per_hit() # Already returns PED - self.cost_per_hit_label.setText(f"{cost_per_hit:.4f} PED") - - # Protection summary - protection = config.get_total_protection() - prot_text = format_protection(protection) - if prot_text == "None": - self.protection_summary_label.setText("No protection") + # Armor metrics (cost per hit) - use new armor system + if self.current_armor_decay > 0: + self.cost_per_hit_label.setText(f"{self.current_armor_decay:.4f} PED") + prot = self.current_armor_protection + prot_text = format_protection(prot) + self.protection_summary_label.setText(f"Total: {prot.get_total():.1f} | {prot_text}") else: - self.protection_summary_label.setText(f"Total: {protection.get_total():.1f} | {prot_text}") + cost_per_hit = config.get_armor_decay_per_hit() + self.cost_per_hit_label.setText(f"{cost_per_hit:.4f} PED") + protection = config.get_total_protection() + prot_text = format_protection(protection) + if prot_text == "None": + self.protection_summary_label.setText("No protection") + else: + self.protection_summary_label.setText(f"Total: {protection.get_total():.1f} | {prot_text}") # Healing metrics - cost_per_heal = config.get_heal_cost_per_use() # Already returns PED + cost_per_heal = config.get_heal_cost_per_use() self.cost_per_heal_label.setText(f"{cost_per_heal:.4f} PED") hp_per_pec = config.get_hp_per_pec()