Lemontropia-Suite/ui/armor_selection_dialog.py

495 lines
19 KiB
Python

"""
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"<b>{slot.value.title()}:</b>")
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)