495 lines
19 KiB
Python
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)
|