feat(ui): add armor set selection to armor selector

- Added tabs to ArmorSelectorDialog: Individual Pieces and Full Sets
- Armor sets show total protection across all 7 pieces
- Selecting a full set auto-populates all 7 armor slots in loadout manager
- Added armor_set_selected signal for full set selection
- Shows summary of equipped pieces and any missing ones
This commit is contained in:
LemonNexus 2026-02-09 17:04:21 +00:00
parent 1ba8acb7e2
commit 1be69b1879
2 changed files with 330 additions and 21 deletions

View File

@ -7,12 +7,12 @@ from decimal import Decimal
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox,
QProgressBar, QGroupBox, QFormLayout QProgressBar, QGroupBox, QFormLayout, QTabWidget, QWidget
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtCore import Qt, QThread, pyqtSignal
from typing import Optional, List 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): class ArmorLoaderThread(QThread):
@ -29,10 +29,25 @@ class ArmorLoaderThread(QThread):
self.error_occurred.emit(str(e)) 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): 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_selected = pyqtSignal(NexusArmor)
armor_set_selected = pyqtSignal(NexusArmorSet) # New signal for full sets
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -40,7 +55,9 @@ class ArmorSelectorDialog(QDialog):
self.setMinimumSize(1000, 700) self.setMinimumSize(1000, 700)
self.all_armors: List[NexusArmor] = [] self.all_armors: List[NexusArmor] = []
self.all_armor_sets: List[NexusArmorSet] = []
self.selected_armor: Optional[NexusArmor] = None self.selected_armor: Optional[NexusArmor] = None
self.selected_armor_set: Optional[NexusArmorSet] = None
self._setup_ui() self._setup_ui()
self._load_data() self._load_data()
@ -49,7 +66,7 @@ class ArmorSelectorDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setSpacing(10) layout.setSpacing(10)
# Status and search # Status
self.status_label = QLabel("Loading armors from Entropia Nexus...") self.status_label = QLabel("Loading armors from Entropia Nexus...")
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
@ -57,20 +74,27 @@ class ArmorSelectorDialog(QDialog):
self.progress.setRange(0, 0) # Indeterminate self.progress.setRange(0, 0) # Indeterminate
layout.addWidget(self.progress) 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
search_layout = QHBoxLayout() search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:")) search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit() 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) self.search_input.textChanged.connect(self._on_search)
search_layout.addWidget(self.search_input) search_layout.addWidget(self.search_input)
self.clear_btn = QPushButton("Clear") self.clear_btn = QPushButton("Clear")
self.clear_btn.clicked.connect(self._clear_search) self.clear_btn.clicked.connect(self._clear_search)
search_layout.addWidget(self.clear_btn) 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 = QTreeWidget()
self.results_tree.setHeaderLabels([ self.results_tree.setHeaderLabels([
"Name", "Type", "Durability", "Name", "Type", "Durability",
@ -82,9 +106,45 @@ class ArmorSelectorDialog(QDialog):
header.setStretchLastSection(False) header.setStretchLastSection(False)
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
self.results_tree.itemDoubleClicked.connect(self._on_double_click) 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") self.preview_group = QGroupBox("Armor Preview")
preview_layout = QFormLayout(self.preview_group) preview_layout = QFormLayout(self.preview_group)
self.preview_name = QLabel("-") self.preview_name = QLabel("-")
@ -106,18 +166,28 @@ class ArmorSelectorDialog(QDialog):
self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
self.ok_button.setEnabled(False) self.ok_button.setEnabled(False)
layout.addWidget(buttons) layout.addWidget(buttons)
# Update preview when tab changes
self.tabs.currentChanged.connect(self._on_tab_changed)
def _load_data(self): 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 = ArmorLoaderThread()
self.loader.armors_loaded.connect(self._on_armors_loaded) self.loader.armors_loaded.connect(self._on_armors_loaded)
self.loader.error_occurred.connect(self._on_load_error) self.loader.error_occurred.connect(self._on_load_error)
self.loader.start() 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]): def _on_armors_loaded(self, armors: List[NexusArmor]):
"""Handle loaded armors.""" """Handle loaded armors."""
self.all_armors = 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.setRange(0, 100)
self.progress.setValue(100) self.progress.setValue(100)
self._populate_results(armors) self._populate_results(armors)
@ -128,8 +198,17 @@ class ArmorSelectorDialog(QDialog):
self.progress.setRange(0, 100) self.progress.setRange(0, 100)
self.progress.setValue(0) 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]): def _populate_results(self, armors: List[NexusArmor]):
"""Populate results tree.""" """Populate results tree with individual armor pieces."""
self.results_tree.clear() self.results_tree.clear()
for armor in armors: for armor in armors:
item = QTreeWidgetItem() item = QTreeWidgetItem()
@ -150,8 +229,32 @@ class ArmorSelectorDialog(QDialog):
item.setData(0, Qt.ItemDataRole.UserRole, armor) item.setData(0, Qt.ItemDataRole.UserRole, armor)
self.results_tree.addTopLevelItem(item) 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): def _on_search(self, text: str):
"""Handle search text change.""" """Handle search text change for individual pieces."""
if not text: if not text:
self._populate_results(self.all_armors) self._populate_results(self.all_armors)
return return
@ -162,26 +265,70 @@ class ArmorSelectorDialog(QDialog):
self.status_label.setText(f"Found {len(filtered)} armors matching '{text}'") self.status_label.setText(f"Found {len(filtered)} armors matching '{text}'")
def _clear_search(self): def _clear_search(self):
"""Clear search.""" """Clear search for individual pieces."""
self.search_input.clear() self.search_input.clear()
self._populate_results(self.all_armors) self._populate_results(self.all_armors)
self.status_label.setText(f"Showing all {len(self.all_armors)} 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): def _on_selection_changed(self):
"""Handle selection change.""" """Handle selection change for individual pieces."""
items = self.results_tree.selectedItems() items = self.results_tree.selectedItems()
if items: if items:
self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole) self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole)
self.selected_armor_set = None
self.ok_button.setEnabled(True) self.ok_button.setEnabled(True)
self._update_preview(self.selected_armor) self._update_preview(self.selected_armor)
else: else:
self.selected_armor = None self.selected_armor = None
self.ok_button.setEnabled(False) 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): def _update_preview(self, armor: NexusArmor):
"""Update preview panel.""" """Update preview panel for individual armor."""
self.preview_name.setText(armor.name) 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)") self.preview_durability.setText(f"{armor.durability} (~{Decimal('20') / (Decimal('1') - Decimal(armor.durability) / Decimal('100000')):.2f} hp/pec)")
prot_parts = [] prot_parts = []
@ -195,15 +342,68 @@ class ArmorSelectorDialog(QDialog):
prot_parts.append(f"Burn:{armor.protection_burn}") prot_parts.append(f"Burn:{armor.protection_burn}")
if armor.protection_cold > 0: if armor.protection_cold > 0:
prot_parts.append(f"Cold:{armor.protection_cold}") 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") 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): def _on_double_click(self, item, column):
"""Handle double click.""" """Handle double click on individual armor."""
self._on_accept() 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): def _on_accept(self):
"""Handle OK button.""" """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: if self.selected_armor:
self.armor_selected.emit(self.selected_armor) self.armor_selected.emit(self.selected_armor)
self.accept() self.accept()

View File

@ -26,7 +26,7 @@ from PyQt6.QtGui import QFont
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
from core.nexus_full_api import ( from core.nexus_full_api import (
get_nexus_api, NexusArmor, NexusHealingTool, NexusPlate, get_nexus_api, NexusArmor, NexusArmorSet, NexusHealingTool, NexusPlate,
NexusAttachment, NexusEnhancer, NexusRing, NexusClothing, NexusPet NexusAttachment, NexusEnhancer, NexusRing, NexusClothing, NexusPet
) )
from core.attachments import ( from core.attachments import (
@ -1590,20 +1590,129 @@ class LoadoutManagerDialog(QDialog):
from ui.armor_selector import ArmorSelectorDialog from ui.armor_selector import ArmorSelectorDialog
dialog = ArmorSelectorDialog(self) dialog = ArmorSelectorDialog(self)
dialog.armor_selected.connect(self._on_api_armor_selected) dialog.armor_selected.connect(self._on_api_armor_selected)
dialog.armor_set_selected.connect(self._on_api_armor_set_selected)
dialog.exec() dialog.exec()
def _on_api_armor_selected(self, armor: NexusArmor): def _on_api_armor_selected(self, armor: NexusArmor):
"""Handle armor selection from API.""" """Handle individual armor piece selection from API."""
# Store selected armor info # Store selected armor info
self._selected_api_armor = armor self._selected_api_armor = armor
QMessageBox.information( QMessageBox.information(
self, self,
"Armor Selected", "Armor Selected",
f"Selected: {armor.name}\n" f"Selected: {armor.name}\n"
f"Type: {armor.type}\n"
f"Durability: {armor.durability}\n" f"Durability: {armor.durability}\n"
f"Protection: Impact {armor.protection_impact}, Cut {armor.protection_cut}, Stab {armor.protection_stab}" 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): def _on_select_healing_from_api(self):
"""Open healing tool selector dialog from Nexus API.""" """Open healing tool selector dialog from Nexus API."""
from ui.healing_selector import HealingSelectorDialog from ui.healing_selector import HealingSelectorDialog