1979 lines
76 KiB
Python
1979 lines
76 KiB
Python
"""
|
|
Lemontropia Suite - Loadout Manager UI v3.0
|
|
Complete armor system with sets, individual pieces, and plating.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import logging
|
|
from dataclasses import dataclass, asdict, field
|
|
from decimal import Decimal, InvalidOperation
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
|
QLineEdit, QComboBox, QLabel, QPushButton,
|
|
QGroupBox, QSpinBox, QMessageBox,
|
|
QListWidget, QListWidgetItem, QSplitter, QWidget,
|
|
QFrame, QScrollArea, QGridLayout, QCheckBox,
|
|
QDialogButtonBox, QTreeWidget, QTreeWidgetItem,
|
|
QHeaderView, QTabWidget, QProgressDialog,
|
|
QStackedWidget, QSizePolicy
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QThread
|
|
from PyQt6.QtGui import QFont
|
|
|
|
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
|
|
from core.nexus_full_api import get_nexus_api, NexusArmor, NexusHealingTool
|
|
from core.attachments import (
|
|
Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber,
|
|
ArmorPlating, Enhancer, can_attach, get_mock_attachments
|
|
)
|
|
from core.armor_system import (
|
|
ArmorSlot, ArmorSet, ArmorPiece, ArmorPlate, EquippedArmor,
|
|
ProtectionProfile, HitResult, calculate_hit_protection,
|
|
get_all_armor_sets, get_all_armor_pieces, get_pieces_by_slot,
|
|
get_mock_plates, format_protection, ALL_ARMOR_SLOTS,
|
|
create_ghost_set, create_shogun_set, create_vigilante_set,
|
|
create_hermes_set, create_pixie_set,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Data Structures
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class AttachmentConfig:
|
|
"""Configuration for an equipped attachment."""
|
|
name: str
|
|
item_id: str
|
|
attachment_type: str
|
|
decay_pec: Decimal
|
|
damage_bonus: Decimal = Decimal("0")
|
|
range_bonus: Decimal = Decimal("0")
|
|
efficiency_bonus: Decimal = Decimal("0")
|
|
protection_bonus: Dict[str, Decimal] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
'name': self.name,
|
|
'item_id': self.item_id,
|
|
'attachment_type': self.attachment_type,
|
|
'decay_pec': str(self.decay_pec),
|
|
'damage_bonus': str(self.damage_bonus),
|
|
'range_bonus': str(self.range_bonus),
|
|
'efficiency_bonus': str(self.efficiency_bonus),
|
|
'protection_bonus': {k: str(v) for k, v in self.protection_bonus.items()},
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "AttachmentConfig":
|
|
return cls(
|
|
name=data['name'],
|
|
item_id=data['item_id'],
|
|
attachment_type=data['attachment_type'],
|
|
decay_pec=Decimal(data['decay_pec']),
|
|
damage_bonus=Decimal(data.get('damage_bonus', '0')),
|
|
range_bonus=Decimal(data.get('range_bonus', '0')),
|
|
efficiency_bonus=Decimal(data.get('efficiency_bonus', '0')),
|
|
protection_bonus={k: Decimal(v) for k, v in data.get('protection_bonus', {}).items()},
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class LoadoutConfig:
|
|
"""Configuration for a hunting loadout with full armor system."""
|
|
name: str
|
|
|
|
# Weapon
|
|
weapon_name: str
|
|
weapon_id: int = 0
|
|
weapon_damage: Decimal = Decimal("0")
|
|
weapon_decay_pec: Decimal = Decimal("0")
|
|
weapon_ammo_pec: Decimal = Decimal("0")
|
|
weapon_dpp: Decimal = Decimal("0")
|
|
|
|
# Weapon Attachments
|
|
weapon_amplifier: Optional[AttachmentConfig] = None
|
|
weapon_scope: Optional[AttachmentConfig] = None
|
|
weapon_absorber: Optional[AttachmentConfig] = None
|
|
|
|
# Armor System
|
|
equipped_armor: Optional[EquippedArmor] = None
|
|
armor_set_name: str = "-- None --"
|
|
|
|
# Legacy armor fields for backward compatibility
|
|
armor_name: str = "-- None --"
|
|
armor_id: int = 0
|
|
armor_decay_pec: Decimal = Decimal("0")
|
|
protection_stab: Decimal = Decimal("0")
|
|
protection_cut: Decimal = Decimal("0")
|
|
protection_impact: Decimal = Decimal("0")
|
|
protection_penetration: Decimal = Decimal("0")
|
|
protection_shrapnel: Decimal = Decimal("0")
|
|
protection_burn: Decimal = Decimal("0")
|
|
protection_cold: Decimal = Decimal("0")
|
|
protection_acid: Decimal = Decimal("0")
|
|
protection_electric: Decimal = Decimal("0")
|
|
|
|
# Healing
|
|
heal_name: str = "-- Custom --"
|
|
heal_cost_pec: Decimal = Decimal("2.0")
|
|
heal_amount: Decimal = Decimal("20")
|
|
|
|
# Settings
|
|
shots_per_hour: int = 3600
|
|
hits_per_hour: int = 720
|
|
heals_per_hour: int = 60
|
|
|
|
def get_total_damage(self) -> Decimal:
|
|
"""Calculate total damage including amplifier."""
|
|
base = self.weapon_damage
|
|
if self.weapon_amplifier:
|
|
base += self.weapon_amplifier.damage_bonus
|
|
return base
|
|
|
|
def get_total_decay_per_shot(self) -> Decimal:
|
|
"""Calculate total decay per shot including attachments."""
|
|
total = self.weapon_decay_pec
|
|
if self.weapon_amplifier:
|
|
total += self.weapon_amplifier.decay_pec
|
|
if self.weapon_scope:
|
|
total += self.weapon_scope.decay_pec
|
|
if self.weapon_absorber:
|
|
total += self.weapon_absorber.decay_pec
|
|
return total
|
|
|
|
def get_total_ammo_per_shot(self) -> Decimal:
|
|
"""Calculate total ammo cost per shot in PEC."""
|
|
total = self.weapon_ammo_pec * Decimal("0.01")
|
|
if self.weapon_amplifier:
|
|
total += self.weapon_amplifier.damage_bonus * Decimal("0.2")
|
|
return total
|
|
|
|
def calculate_dpp(self) -> Decimal:
|
|
"""Calculate Damage Per Pec (DPP) with all attachments."""
|
|
total_damage = self.get_total_damage()
|
|
total_cost = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
if total_cost == 0:
|
|
return Decimal("0")
|
|
return total_damage / total_cost
|
|
|
|
def calculate_weapon_cost_per_hour(self) -> Decimal:
|
|
"""Calculate weapon cost per hour."""
|
|
cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
return cost_per_shot * Decimal(self.shots_per_hour)
|
|
|
|
def calculate_armor_cost_per_hour(self) -> Decimal:
|
|
"""Calculate armor cost per hour using the equipped armor system."""
|
|
if self.equipped_armor:
|
|
return self.equipped_armor.get_total_decay_per_hit() * Decimal(self.hits_per_hour)
|
|
# Legacy fallback
|
|
return self.armor_decay_pec * Decimal(self.hits_per_hour)
|
|
|
|
def calculate_heal_cost_per_hour(self) -> Decimal:
|
|
"""Calculate healing cost per hour."""
|
|
return self.heal_cost_pec * Decimal(self.heals_per_hour)
|
|
|
|
def calculate_total_cost_per_hour(self) -> Decimal:
|
|
"""Calculate total PED cost per hour."""
|
|
weapon_cost = self.calculate_weapon_cost_per_hour()
|
|
armor_cost = self.calculate_armor_cost_per_hour()
|
|
heal_cost = self.calculate_heal_cost_per_hour()
|
|
|
|
total_pec = weapon_cost + armor_cost + heal_cost
|
|
return total_pec / Decimal("100")
|
|
|
|
def calculate_break_even(self, mob_health: Decimal) -> Decimal:
|
|
"""Calculate break-even loot value for a mob."""
|
|
total_damage = self.get_total_damage()
|
|
shots_to_kill = mob_health / total_damage if total_damage > 0 else Decimal("1")
|
|
if shots_to_kill < 1:
|
|
shots_to_kill = Decimal("1")
|
|
|
|
cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
total_cost_pec = shots_to_kill * cost_per_shot
|
|
return total_cost_pec / Decimal("100")
|
|
|
|
def get_total_protection(self) -> ProtectionProfile:
|
|
"""Get total protection from equipped armor."""
|
|
if self.equipped_armor:
|
|
return self.equipped_armor.get_total_protection()
|
|
# Legacy fallback
|
|
return ProtectionProfile(
|
|
stab=self.protection_stab,
|
|
cut=self.protection_cut,
|
|
impact=self.protection_impact,
|
|
penetration=self.protection_penetration,
|
|
shrapnel=self.protection_shrapnel,
|
|
burn=self.protection_burn,
|
|
cold=self.protection_cold,
|
|
acid=self.protection_acid,
|
|
electric=self.protection_electric,
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for JSON serialization."""
|
|
data = {
|
|
k: str(v) if isinstance(v, Decimal) else v
|
|
for k, v in asdict(self).items()
|
|
}
|
|
# Handle attachment configs
|
|
if self.weapon_amplifier:
|
|
data['weapon_amplifier'] = self.weapon_amplifier.to_dict()
|
|
if self.weapon_scope:
|
|
data['weapon_scope'] = self.weapon_scope.to_dict()
|
|
if self.weapon_absorber:
|
|
data['weapon_absorber'] = self.weapon_absorber.to_dict()
|
|
# Handle equipped armor
|
|
if self.equipped_armor:
|
|
data['equipped_armor'] = self.equipped_armor.to_dict()
|
|
return data
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "LoadoutConfig":
|
|
"""Create LoadoutConfig from dictionary."""
|
|
decimal_fields = [
|
|
'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec', 'weapon_dpp',
|
|
'armor_decay_pec', 'heal_cost_pec', 'heal_amount', 'protection_stab',
|
|
'protection_cut', 'protection_impact', 'protection_penetration',
|
|
'protection_shrapnel', 'protection_burn', 'protection_cold',
|
|
'protection_acid', 'protection_electric'
|
|
]
|
|
|
|
for field in decimal_fields:
|
|
if field in data:
|
|
data[field] = Decimal(data[field])
|
|
|
|
# Handle integer fields
|
|
int_fields = ['weapon_id', 'armor_id', 'shots_per_hour', 'hits_per_hour', 'heals_per_hour']
|
|
for field in int_fields:
|
|
if field in data:
|
|
data[field] = int(data[field])
|
|
|
|
# Handle attachment configs
|
|
if 'weapon_amplifier' in data and data['weapon_amplifier']:
|
|
data['weapon_amplifier'] = AttachmentConfig.from_dict(data['weapon_amplifier'])
|
|
else:
|
|
data['weapon_amplifier'] = None
|
|
|
|
if 'weapon_scope' in data and data['weapon_scope']:
|
|
data['weapon_scope'] = AttachmentConfig.from_dict(data['weapon_scope'])
|
|
else:
|
|
data['weapon_scope'] = None
|
|
|
|
if 'weapon_absorber' in data and data['weapon_absorber']:
|
|
data['weapon_absorber'] = AttachmentConfig.from_dict(data['weapon_absorber'])
|
|
else:
|
|
data['weapon_absorber'] = None
|
|
|
|
# Handle equipped armor
|
|
if 'equipped_armor' in data and data['equipped_armor']:
|
|
data['equipped_armor'] = EquippedArmor.from_dict(data['equipped_armor'])
|
|
else:
|
|
data['equipped_armor'] = None
|
|
|
|
# Handle legacy configs
|
|
if 'heal_name' not in data:
|
|
data['heal_name'] = '-- Custom --'
|
|
if 'armor_set_name' not in data:
|
|
data['armor_set_name'] = '-- None --'
|
|
|
|
return cls(**data)
|
|
|
|
|
|
# ============================================================================
|
|
# Healing Tools Data - Using real database
|
|
# ============================================================================
|
|
|
|
def get_healing_tools_data():
|
|
"""Get healing tools from the real database."""
|
|
try:
|
|
from core.healing_tools import HEALING_TOOLS
|
|
return [
|
|
{
|
|
"name": tool.name,
|
|
"cost": tool.decay_pec,
|
|
"amount": tool.heal_amount,
|
|
"is_chip": tool.is_chip
|
|
}
|
|
for tool in HEALING_TOOLS
|
|
]
|
|
except ImportError:
|
|
# Fallback to mock data if import fails
|
|
return [
|
|
{"name": "Vivo T10", "cost": Decimal("0.815"), "amount": Decimal("10")},
|
|
{"name": "Vivo S10", "cost": Decimal("1.705"), "amount": Decimal("21")},
|
|
{"name": "Hedoc MM10", "cost": Decimal("2.09"), "amount": Decimal("44")},
|
|
{"name": "Adjusted Restoration Chip", "cost": Decimal("2.88"), "amount": Decimal("60")},
|
|
{"name": "Restoration Chip IV (L)", "cost": Decimal("2.8"), "amount": Decimal("45")},
|
|
]
|
|
|
|
|
|
# Legacy mock data for compatibility
|
|
MOCK_HEALING = get_healing_tools_data()
|
|
|
|
|
|
# ============================================================================
|
|
# Custom Widgets
|
|
# ============================================================================
|
|
|
|
class DecimalLineEdit(QLineEdit):
|
|
"""Line edit with decimal validation."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setPlaceholderText("0.00")
|
|
|
|
def get_decimal(self) -> Decimal:
|
|
"""Get value as Decimal, returns 0 on invalid input."""
|
|
text = self.text().strip()
|
|
if not text:
|
|
return Decimal("0")
|
|
try:
|
|
return Decimal(text)
|
|
except InvalidOperation:
|
|
return Decimal("0")
|
|
|
|
def set_decimal(self, value: Decimal):
|
|
"""Set value from Decimal."""
|
|
self.setText(str(value))
|
|
|
|
|
|
class DarkGroupBox(QGroupBox):
|
|
"""Group box with dark theme styling."""
|
|
|
|
def __init__(self, title: str, parent=None):
|
|
super().__init__(title, parent)
|
|
self.setStyleSheet("""
|
|
QGroupBox {
|
|
color: #e0e0e0;
|
|
border: 2px solid #3d3d3d;
|
|
border-radius: 6px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
font-weight: bold;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}
|
|
""")
|
|
|
|
|
|
class ArmorSlotWidget(QWidget):
|
|
"""Widget for configuring a single armor slot with piece and plate."""
|
|
|
|
piece_changed = pyqtSignal()
|
|
plate_changed = pyqtSignal()
|
|
|
|
def __init__(self, slot: ArmorSlot, parent=None):
|
|
super().__init__(parent)
|
|
self.slot = slot
|
|
self.current_piece: Optional[ArmorPiece] = None
|
|
self.current_plate: Optional[ArmorPlate] = None
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(5, 2, 5, 2)
|
|
layout.setSpacing(10)
|
|
|
|
slot_name = self._get_slot_display_name()
|
|
|
|
# Slot label
|
|
self.slot_label = QLabel(f"<b>{slot_name}:</b>")
|
|
self.slot_label.setFixedWidth(100)
|
|
layout.addWidget(self.slot_label)
|
|
|
|
# Armor piece selector
|
|
self.piece_combo = QComboBox()
|
|
self.piece_combo.setMinimumWidth(180)
|
|
self.piece_combo.currentTextChanged.connect(self._on_piece_changed)
|
|
layout.addWidget(self.piece_combo)
|
|
|
|
# Protection display
|
|
self.protection_label = QLabel("-")
|
|
self.protection_label.setStyleSheet("color: #888888; font-size: 11px;")
|
|
self.protection_label.setFixedWidth(120)
|
|
layout.addWidget(self.protection_label)
|
|
|
|
# Plate selector
|
|
self.plate_combo = QComboBox()
|
|
self.plate_combo.setMinimumWidth(150)
|
|
self.plate_combo.currentTextChanged.connect(self._on_plate_changed)
|
|
layout.addWidget(self.plate_combo)
|
|
|
|
# Total protection
|
|
self.total_label = QLabel("Total: 0")
|
|
self.total_label.setStyleSheet("color: #4caf50; font-weight: bold;")
|
|
self.total_label.setFixedWidth(80)
|
|
layout.addWidget(self.total_label)
|
|
|
|
layout.addStretch()
|
|
|
|
# Populate combos
|
|
self._populate_pieces()
|
|
self._populate_plates()
|
|
|
|
def _get_slot_display_name(self) -> str:
|
|
"""Get human-readable slot name (matches Entropia Nexus)."""
|
|
names = {
|
|
ArmorSlot.HEAD: "Head",
|
|
ArmorSlot.TORSO: "Torso",
|
|
ArmorSlot.ARMS: "Arms",
|
|
ArmorSlot.HANDS: "Hands",
|
|
ArmorSlot.LEGS: "Legs",
|
|
ArmorSlot.SHINS: "Shins",
|
|
ArmorSlot.FEET: "Feet",
|
|
}
|
|
return names.get(self.slot, self.slot.value)
|
|
|
|
def _populate_pieces(self):
|
|
"""Populate armor piece combo."""
|
|
self.piece_combo.clear()
|
|
self.piece_combo.addItem("-- Empty --")
|
|
|
|
# Get pieces for this slot
|
|
pieces = get_pieces_by_slot(self.slot)
|
|
for piece in pieces:
|
|
display = f"{piece.name} ({piece.set_name})"
|
|
self.piece_combo.addItem(display, piece)
|
|
|
|
def _populate_plates(self):
|
|
"""Populate plate combo."""
|
|
self.plate_combo.clear()
|
|
self.plate_combo.addItem("-- No Plate --")
|
|
|
|
plates = get_mock_plates()
|
|
for plate in plates:
|
|
display = f"{plate.name} (+{plate.get_total_protection()})"
|
|
self.plate_combo.addItem(display, plate)
|
|
|
|
def _on_piece_changed(self, text: str):
|
|
"""Handle armor piece selection."""
|
|
if text == "-- Empty --":
|
|
self.current_piece = None
|
|
self.protection_label.setText("-")
|
|
else:
|
|
self.current_piece = self.piece_combo.currentData()
|
|
if self.current_piece:
|
|
prot = format_protection(self.current_piece.protection)
|
|
self.protection_label.setText(prot)
|
|
|
|
self._update_total()
|
|
self.piece_changed.emit()
|
|
|
|
def _on_plate_changed(self, text: str):
|
|
"""Handle plate selection."""
|
|
if text == "-- No Plate --":
|
|
self.current_plate = None
|
|
else:
|
|
self.current_plate = self.plate_combo.currentData()
|
|
|
|
self._update_total()
|
|
self.plate_changed.emit()
|
|
|
|
def _update_total(self):
|
|
"""Update total protection display."""
|
|
total = Decimal("0")
|
|
|
|
if self.current_piece:
|
|
total += self.current_piece.protection.get_total()
|
|
|
|
if self.current_plate:
|
|
total += self.current_plate.get_total_protection()
|
|
|
|
self.total_label.setText(f"Total: {total}")
|
|
|
|
def get_piece(self) -> Optional[ArmorPiece]:
|
|
"""Get selected armor piece."""
|
|
return self.current_piece
|
|
|
|
def get_plate(self) -> Optional[ArmorPlate]:
|
|
"""Get selected plate."""
|
|
return self.current_plate
|
|
|
|
def set_piece(self, piece: Optional[ArmorPiece]):
|
|
"""Set selected armor piece."""
|
|
if piece is None:
|
|
self.piece_combo.setCurrentIndex(0)
|
|
return
|
|
|
|
# Find and select the piece
|
|
for i in range(self.piece_combo.count()):
|
|
data = self.piece_combo.itemData(i)
|
|
if data and data.item_id == piece.item_id:
|
|
self.piece_combo.setCurrentIndex(i)
|
|
return
|
|
|
|
self.piece_combo.setCurrentIndex(0)
|
|
|
|
def set_plate(self, plate: Optional[ArmorPlate]):
|
|
"""Set selected plate."""
|
|
if plate is None:
|
|
self.plate_combo.setCurrentIndex(0)
|
|
return
|
|
|
|
# Find and select the plate
|
|
for i in range(self.plate_combo.count()):
|
|
data = self.plate_combo.itemData(i)
|
|
if data and data.item_id == plate.item_id:
|
|
self.plate_combo.setCurrentIndex(i)
|
|
return
|
|
|
|
self.plate_combo.setCurrentIndex(0)
|
|
|
|
def get_total_protection(self) -> ProtectionProfile:
|
|
"""Get total protection for this slot."""
|
|
total = ProtectionProfile()
|
|
if self.current_piece:
|
|
total = total.add(self.current_piece.protection)
|
|
if self.current_plate:
|
|
total = total.add(self.current_plate.protection)
|
|
return total
|
|
|
|
def get_total_decay(self) -> Decimal:
|
|
"""Get total decay per hit for this slot (estimated)."""
|
|
# Estimate based on typical hit of 10 hp
|
|
typical_hit = Decimal("10")
|
|
decay = Decimal("0")
|
|
|
|
if self.current_piece:
|
|
# Armor only decays for damage it actually absorbs
|
|
armor_absorb = min(typical_hit, self.current_piece.protection.get_total())
|
|
decay += self.current_piece.get_decay_for_damage(armor_absorb)
|
|
|
|
if self.current_plate:
|
|
# Plate only decays for damage it actually absorbs
|
|
plate_absorb = min(typical_hit, self.current_plate.get_total_protection())
|
|
decay += self.current_plate.get_decay_for_damage(plate_absorb)
|
|
|
|
return decay
|
|
|
|
|
|
# ============================================================================
|
|
# Gear Loader Threads
|
|
# ============================================================================
|
|
|
|
class WeaponLoaderThread(QThread):
|
|
"""Thread to load weapons from API."""
|
|
weapons_loaded = pyqtSignal(list)
|
|
error_occurred = pyqtSignal(str)
|
|
|
|
def run(self):
|
|
try:
|
|
api = EntropiaNexusAPI()
|
|
weapons = api.get_all_weapons()
|
|
self.weapons_loaded.emit(weapons)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load weapons: {e}")
|
|
self.error_occurred.emit(str(e))
|
|
|
|
|
|
class ArmorLoaderThread(QThread):
|
|
"""Thread to load armors from API."""
|
|
armors_loaded = pyqtSignal(list)
|
|
error_occurred = pyqtSignal(str)
|
|
|
|
def run(self):
|
|
try:
|
|
api = EntropiaNexusAPI()
|
|
armors = api.get_all_armors()
|
|
self.armors_loaded.emit(armors)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load armors: {e}")
|
|
self.error_occurred.emit(str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# Weapon Selector Dialog
|
|
# ============================================================================
|
|
|
|
class WeaponSelectorDialog(QDialog):
|
|
"""Dialog for selecting weapons from Entropia Nexus API."""
|
|
|
|
weapon_selected = pyqtSignal(object)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Select Weapon - Entropia Nexus")
|
|
self.setMinimumSize(900, 600)
|
|
self.weapons = []
|
|
self.selected_weapon = None
|
|
self.api = EntropiaNexusAPI()
|
|
|
|
self._setup_ui()
|
|
self._load_data()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(10)
|
|
|
|
self.status_label = QLabel("Loading weapons from Entropia Nexus...")
|
|
layout.addWidget(self.status_label)
|
|
|
|
search_layout = QHBoxLayout()
|
|
search_layout.addWidget(QLabel("Search:"))
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Search weapons by name...")
|
|
self.search_input.returnPressed.connect(self._on_search)
|
|
search_layout.addWidget(self.search_input)
|
|
self.search_btn = QPushButton("Search")
|
|
self.search_btn.clicked.connect(self._on_search)
|
|
search_layout.addWidget(self.search_btn)
|
|
layout.addLayout(search_layout)
|
|
|
|
self.results_tree = QTreeWidget()
|
|
self.results_tree.setHeaderLabels([
|
|
"Name", "Type", "Category", "Damage", "DPP", "Decay", "Ammo", "Cost/h"
|
|
])
|
|
header = self.results_tree.header()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
for i in range(1, 8):
|
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
|
|
header.resizeSection(1, 80)
|
|
header.resizeSection(2, 80)
|
|
header.resizeSection(3, 60)
|
|
header.resizeSection(4, 60)
|
|
header.resizeSection(5, 70)
|
|
header.resizeSection(6, 60)
|
|
header.resizeSection(7, 70)
|
|
|
|
self.results_tree.setAlternatingRowColors(True)
|
|
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
|
|
layout.addWidget(self.results_tree)
|
|
|
|
self.preview_group = DarkGroupBox("Weapon Stats")
|
|
self.preview_layout = QFormLayout(self.preview_group)
|
|
self.preview_layout.addRow("Select a weapon to view stats", QLabel(""))
|
|
layout.addWidget(self.preview_group)
|
|
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_accept)
|
|
button_box.rejected.connect(self.reject)
|
|
self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok)
|
|
self.ok_btn.setEnabled(False)
|
|
self.ok_btn.setText("Select Weapon")
|
|
layout.addWidget(button_box)
|
|
|
|
def _load_data(self):
|
|
"""Load weapons asynchronously."""
|
|
self.loader = WeaponLoaderThread()
|
|
self.loader.weapons_loaded.connect(self._on_data_loaded)
|
|
self.loader.error_occurred.connect(self._on_load_error)
|
|
self.loader.start()
|
|
|
|
def _on_data_loaded(self, weapons):
|
|
"""Handle loaded weapons."""
|
|
self.weapons = weapons
|
|
self.status_label.setText(f"Loaded {len(weapons):,} weapons from Entropia Nexus")
|
|
self._populate_tree(weapons[:200])
|
|
|
|
def _on_load_error(self, error):
|
|
"""Handle load error."""
|
|
self.status_label.setText(f"Error loading weapons: {error}")
|
|
QMessageBox.critical(self, "Error", f"Failed to load weapons: {error}")
|
|
|
|
def _populate_tree(self, weapons):
|
|
"""Populate tree with weapons."""
|
|
self.results_tree.clear()
|
|
|
|
for w in weapons:
|
|
item = QTreeWidgetItem([
|
|
w.name,
|
|
w.type,
|
|
w.category,
|
|
str(w.total_damage),
|
|
f"{w.dpp:.2f}",
|
|
f"{w.decay:.2f}" if w.decay else "-",
|
|
str(w.ammo_burn) if w.ammo_burn else "-",
|
|
f"{w.cost_per_hour:.0f}"
|
|
])
|
|
item.setData(0, Qt.ItemDataRole.UserRole, w)
|
|
self.results_tree.addTopLevelItem(item)
|
|
|
|
def _on_search(self):
|
|
"""Search weapons."""
|
|
query = self.search_input.text().strip().lower()
|
|
if not query:
|
|
self._populate_tree(self.weapons[:200])
|
|
return
|
|
|
|
results = [w for w in self.weapons if query in w.name.lower()]
|
|
self._populate_tree(results)
|
|
self.status_label.setText(f"Found {len(results)} weapons matching '{query}'")
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
selected = self.results_tree.selectedItems()
|
|
if selected:
|
|
weapon = selected[0].data(0, Qt.ItemDataRole.UserRole)
|
|
self.selected_weapon = weapon
|
|
self.ok_btn.setEnabled(True)
|
|
self._update_preview(weapon)
|
|
else:
|
|
self.selected_weapon = None
|
|
self.ok_btn.setEnabled(False)
|
|
|
|
def _update_preview(self, w):
|
|
"""Update stats preview."""
|
|
while self.preview_layout.rowCount() > 0:
|
|
self.preview_layout.removeRow(0)
|
|
|
|
self.preview_layout.addRow("Name:", QLabel(w.name))
|
|
self.preview_layout.addRow("Type:", QLabel(f"{w.type} {w.category}"))
|
|
self.preview_layout.addRow("Damage:", QLabel(str(w.total_damage)))
|
|
self.preview_layout.addRow("DPP:", QLabel(f"{w.dpp:.3f}"))
|
|
self.preview_layout.addRow("Decay:", QLabel(f"{w.decay:.3f} PEC/shot" if w.decay else "-"))
|
|
self.preview_layout.addRow("Ammo:", QLabel(f"{w.ammo_burn} units/shot" if w.ammo_burn else "-"))
|
|
self.preview_layout.addRow("Cost/Hour:", QLabel(f"{w.cost_per_hour:.2f} PED"))
|
|
if w.efficiency:
|
|
self.preview_layout.addRow("Efficiency:", QLabel(f"{w.efficiency:.1f}%"))
|
|
|
|
def _on_double_click(self, item, column):
|
|
"""Handle double click."""
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
"""Handle OK button."""
|
|
if self.selected_weapon:
|
|
self.weapon_selected.emit(self.selected_weapon)
|
|
self.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Armor Selector Dialog
|
|
# ============================================================================
|
|
|
|
class ArmorSelectorDialog(QDialog):
|
|
"""Dialog for selecting armors from Entropia Nexus API."""
|
|
|
|
armor_selected = pyqtSignal(object)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Select Armor - Entropia Nexus")
|
|
self.setMinimumSize(900, 600)
|
|
self.armors = []
|
|
self.selected_armor = None
|
|
self.api = EntropiaNexusAPI()
|
|
|
|
self._setup_ui()
|
|
self._load_data()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(10)
|
|
|
|
self.status_label = QLabel("Loading armors from Entropia Nexus...")
|
|
layout.addWidget(self.status_label)
|
|
|
|
search_layout = QHBoxLayout()
|
|
search_layout.addWidget(QLabel("Search:"))
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Search armors by name...")
|
|
self.search_input.returnPressed.connect(self._on_search)
|
|
search_layout.addWidget(self.search_input)
|
|
self.search_btn = QPushButton("Search")
|
|
self.search_btn.clicked.connect(self._on_search)
|
|
search_layout.addWidget(self.search_btn)
|
|
layout.addLayout(search_layout)
|
|
|
|
self.results_tree = QTreeWidget()
|
|
self.results_tree.setHeaderLabels([
|
|
"Name", "Type", "Durability", "Impact", "Cut", "Stab", "Burn", "Cold"
|
|
])
|
|
header = self.results_tree.header()
|
|
header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setStretchLastSection(False)
|
|
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
|
|
layout.addWidget(self.results_tree)
|
|
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
buttons.accepted.connect(self._on_accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def _load_data(self):
|
|
"""Load armors from API."""
|
|
try:
|
|
self.armors = self.api.get_all_armors()
|
|
self._populate_results(self.armors)
|
|
self.status_label.setText(f"Loaded {len(self.armors)} armors from Entropia Nexus")
|
|
except Exception as e:
|
|
self.status_label.setText(f"Error loading armors: {e}")
|
|
|
|
def _populate_results(self, armors):
|
|
"""Populate results tree."""
|
|
self.results_tree.clear()
|
|
for armor in armors:
|
|
item = QTreeWidgetItem()
|
|
item.setText(0, armor.name)
|
|
item.setText(1, armor.type or "Unknown")
|
|
item.setText(2, str(armor.durability))
|
|
item.setText(3, str(armor.protection_impact))
|
|
item.setText(4, str(armor.protection_cut))
|
|
item.setText(5, str(armor.protection_stab))
|
|
item.setText(6, str(armor.protection_burn))
|
|
item.setText(7, str(armor.protection_cold))
|
|
item.setData(0, Qt.ItemDataRole.UserRole, armor)
|
|
self.results_tree.addTopLevelItem(item)
|
|
|
|
def _on_search(self):
|
|
"""Handle search."""
|
|
query = self.search_input.text().lower()
|
|
if not query:
|
|
self._populate_results(self.armors)
|
|
return
|
|
|
|
filtered = [a for a in self.armors if query in a.name.lower()]
|
|
self._populate_results(filtered)
|
|
self.status_label.setText(f"Found {len(filtered)} armors matching '{query}'")
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
items = self.results_tree.selectedItems()
|
|
if items:
|
|
self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
def _on_double_click(self, item, column):
|
|
"""Handle double click."""
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
"""Handle OK button."""
|
|
if self.selected_armor:
|
|
self.armor_selected.emit(self.selected_armor)
|
|
self.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Main Loadout Manager Dialog
|
|
# ============================================================================
|
|
|
|
class LoadoutManagerDialog(QDialog):
|
|
"""Main dialog for managing hunting loadouts with full armor system."""
|
|
|
|
loadout_saved = pyqtSignal(str)
|
|
|
|
def __init__(self, parent=None, config_dir: Optional[str] = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Lemontropia Suite - Loadout Manager v3.0")
|
|
self.setMinimumSize(1100, 900)
|
|
|
|
if config_dir is None:
|
|
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
|
|
else:
|
|
self.config_dir = Path(config_dir)
|
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.current_loadout: Optional[LoadoutConfig] = None
|
|
self.current_weapon: Optional[WeaponStats] = None
|
|
self.current_armor_set: Optional[ArmorSet] = None
|
|
self.equipped_armor: Optional[EquippedArmor] = None
|
|
|
|
# Armor slot widgets
|
|
self.slot_widgets: Dict[ArmorSlot, ArmorSlotWidget] = {}
|
|
|
|
self._apply_dark_theme()
|
|
self._create_widgets()
|
|
self._create_layout()
|
|
self._connect_signals()
|
|
self._load_saved_loadouts()
|
|
self._populate_armor_sets()
|
|
self._populate_healing_data()
|
|
|
|
def _apply_dark_theme(self):
|
|
"""Apply dark theme styling."""
|
|
self.setStyleSheet("""
|
|
QDialog {
|
|
background-color: #1e1e1e;
|
|
}
|
|
QLabel {
|
|
color: #e0e0e0;
|
|
}
|
|
QLineEdit {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
padding: 5px;
|
|
}
|
|
QLineEdit:disabled {
|
|
background-color: #252525;
|
|
color: #888888;
|
|
border: 1px solid #2d2d2d;
|
|
}
|
|
QLineEdit:focus {
|
|
border: 1px solid #4a90d9;
|
|
}
|
|
QComboBox {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
padding: 5px;
|
|
min-width: 150px;
|
|
}
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
selection-background-color: #4a90d9;
|
|
}
|
|
QPushButton {
|
|
background-color: #3d3d3d;
|
|
color: #e0e0e0;
|
|
border: 1px solid #4d4d4d;
|
|
border-radius: 4px;
|
|
padding: 8px 16px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #4d4d4d;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #5d5d5d;
|
|
}
|
|
QPushButton#saveButton {
|
|
background-color: #2e7d32;
|
|
border-color: #4caf50;
|
|
}
|
|
QPushButton#saveButton:hover {
|
|
background-color: #4caf50;
|
|
}
|
|
QPushButton#deleteButton {
|
|
background-color: #7d2e2e;
|
|
border-color: #f44336;
|
|
}
|
|
QPushButton#deleteButton:hover {
|
|
background-color: #f44336;
|
|
}
|
|
QPushButton#selectButton {
|
|
background-color: #1565c0;
|
|
border-color: #2196f3;
|
|
}
|
|
QPushButton#selectButton:hover {
|
|
background-color: #2196f3;
|
|
}
|
|
QPushButton#clearButton {
|
|
background-color: #5d4037;
|
|
border-color: #8d6e63;
|
|
}
|
|
QPushButton#clearButton:hover {
|
|
background-color: #8d6e63;
|
|
}
|
|
QListWidget {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
}
|
|
QListWidget::item:selected {
|
|
background-color: #4a90d9;
|
|
}
|
|
QScrollArea {
|
|
border: none;
|
|
}
|
|
QTabWidget::pane {
|
|
border: 1px solid #3d3d3d;
|
|
background-color: #1e1e1e;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
padding: 8px 16px;
|
|
border: 1px solid #3d3d3d;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #4a90d9;
|
|
}
|
|
""")
|
|
|
|
def _create_widgets(self):
|
|
"""Create all UI widgets."""
|
|
# Loadout name
|
|
self.loadout_name_edit = QLineEdit()
|
|
self.loadout_name_edit.setPlaceholderText("Enter loadout name...")
|
|
|
|
# Activity settings
|
|
self.shots_per_hour_spin = QSpinBox()
|
|
self.shots_per_hour_spin.setRange(1, 20000)
|
|
self.shots_per_hour_spin.setValue(3600)
|
|
self.shots_per_hour_spin.setSuffix(" /hr")
|
|
|
|
self.hits_per_hour_spin = QSpinBox()
|
|
self.hits_per_hour_spin.setRange(0, 5000)
|
|
self.hits_per_hour_spin.setValue(720)
|
|
self.hits_per_hour_spin.setSuffix(" /hr")
|
|
|
|
self.heals_per_hour_spin = QSpinBox()
|
|
self.heals_per_hour_spin.setRange(0, 500)
|
|
self.heals_per_hour_spin.setValue(60)
|
|
self.heals_per_hour_spin.setSuffix(" /hr")
|
|
|
|
# Weapon section
|
|
self.weapon_group = DarkGroupBox("🔫 Weapon Configuration")
|
|
self.select_weapon_btn = QPushButton("🔍 Select from Entropia Nexus")
|
|
self.select_weapon_btn.setObjectName("selectButton")
|
|
self.weapon_name_label = QLabel("No weapon selected")
|
|
self.weapon_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;")
|
|
|
|
self.weapon_damage_edit = DecimalLineEdit()
|
|
self.weapon_decay_edit = DecimalLineEdit()
|
|
self.weapon_ammo_edit = DecimalLineEdit()
|
|
self.dpp_label = QLabel("0.0000")
|
|
self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 16px;")
|
|
|
|
# Weapon attachments
|
|
self.attach_amp_btn = QPushButton("⚡ Add Amplifier")
|
|
self.attach_scope_btn = QPushButton("🔭 Add Scope")
|
|
self.attach_absorber_btn = QPushButton("🛡️ Add Absorber")
|
|
self.amp_label = QLabel("None")
|
|
self.scope_label = QLabel("None")
|
|
self.absorber_label = QLabel("None")
|
|
self.remove_amp_btn = QPushButton("✕")
|
|
self.remove_scope_btn = QPushButton("✕")
|
|
self.remove_absorber_btn = QPushButton("✕")
|
|
self.remove_amp_btn.setFixedWidth(30)
|
|
self.remove_scope_btn.setFixedWidth(30)
|
|
self.remove_absorber_btn.setFixedWidth(30)
|
|
|
|
# Armor section - NEW COMPLETE SYSTEM
|
|
self.armor_group = DarkGroupBox("🛡️ Armor Configuration")
|
|
|
|
# Armor set selector
|
|
self.armor_set_combo = QComboBox()
|
|
self.armor_set_combo.setMinimumWidth(250)
|
|
|
|
self.equip_set_btn = QPushButton("Equip Full Set")
|
|
self.equip_set_btn.setObjectName("selectButton")
|
|
self.clear_armor_btn = QPushButton("Clear All")
|
|
self.clear_armor_btn.setObjectName("clearButton")
|
|
|
|
# Armor protection summary
|
|
self.armor_summary_label = QLabel("No armor equipped")
|
|
self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;")
|
|
|
|
# Create slot widgets
|
|
for slot in ALL_ARMOR_SLOTS:
|
|
self.slot_widgets[slot] = ArmorSlotWidget(slot)
|
|
self.slot_widgets[slot].piece_changed.connect(self._on_armor_changed)
|
|
self.slot_widgets[slot].plate_changed.connect(self._on_armor_changed)
|
|
|
|
# Healing section
|
|
self.heal_group = DarkGroupBox("💊 Healing Configuration")
|
|
self.heal_combo = QComboBox()
|
|
self.heal_cost_edit = DecimalLineEdit()
|
|
self.heal_amount_edit = DecimalLineEdit()
|
|
|
|
# Cost summary
|
|
self.summary_group = DarkGroupBox("📊 Cost Summary")
|
|
self.weapon_cost_label = QLabel("0.00 PEC/hr")
|
|
self.armor_cost_label = QLabel("0.00 PEC/hr")
|
|
self.heal_cost_label = QLabel("0.00 PEC/hr")
|
|
self.total_cost_label = QLabel("0.00 PED/hr")
|
|
self.total_cost_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 18px;")
|
|
self.total_dpp_label = QLabel("0.0000")
|
|
self.total_dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 18px;")
|
|
|
|
# Protection summary
|
|
self.protection_summary_label = QLabel("No protection")
|
|
self.protection_summary_label.setStyleSheet("color: #4a90d9; font-size: 12px;")
|
|
|
|
# Break-even calculator
|
|
self.mob_health_edit = DecimalLineEdit()
|
|
self.mob_health_edit.set_decimal(Decimal("100"))
|
|
self.calc_break_even_btn = QPushButton("Calculate")
|
|
self.break_even_label = QLabel("Break-even: 0.00 PED")
|
|
self.break_even_label.setStyleSheet("color: #4caf50;")
|
|
|
|
# Saved loadouts list
|
|
self.saved_list = QListWidget()
|
|
|
|
# Buttons
|
|
self.save_btn = QPushButton("💾 Save Loadout")
|
|
self.save_btn.setObjectName("saveButton")
|
|
self.load_btn = QPushButton("📂 Load Selected")
|
|
self.delete_btn = QPushButton("🗑️ Delete")
|
|
self.delete_btn.setObjectName("deleteButton")
|
|
self.new_btn = QPushButton("🆕 New Loadout")
|
|
self.close_btn = QPushButton("❌ Close")
|
|
self.refresh_btn = QPushButton("🔄 Refresh")
|
|
|
|
def _create_layout(self):
|
|
"""Create the main layout."""
|
|
main_layout = QHBoxLayout(self)
|
|
main_layout.setSpacing(15)
|
|
main_layout.setContentsMargins(15, 15, 15, 15)
|
|
|
|
# Left panel - Saved loadouts
|
|
left_panel = QWidget()
|
|
left_layout = QVBoxLayout(left_panel)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
saved_label = QLabel("💼 Saved Loadouts")
|
|
saved_label.setFont(QFont("Arial", 12, QFont.Weight.Bold))
|
|
left_layout.addWidget(saved_label)
|
|
|
|
left_layout.addWidget(self.saved_list)
|
|
|
|
left_btn_layout = QHBoxLayout()
|
|
left_btn_layout.addWidget(self.load_btn)
|
|
left_btn_layout.addWidget(self.delete_btn)
|
|
left_layout.addLayout(left_btn_layout)
|
|
|
|
left_layout.addWidget(self.refresh_btn)
|
|
left_layout.addWidget(self.new_btn)
|
|
left_layout.addStretch()
|
|
left_layout.addWidget(self.close_btn)
|
|
|
|
# Right panel - Configuration
|
|
right_scroll = QScrollArea()
|
|
right_scroll.setWidgetResizable(True)
|
|
right_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
right_widget = QWidget()
|
|
right_layout = QVBoxLayout(right_widget)
|
|
right_layout.setContentsMargins(0, 0, 10, 0)
|
|
|
|
# Loadout name header
|
|
name_layout = QHBoxLayout()
|
|
name_label = QLabel("Loadout Name:")
|
|
name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
|
name_layout.addWidget(name_label)
|
|
name_layout.addWidget(self.loadout_name_edit, stretch=1)
|
|
right_layout.addLayout(name_layout)
|
|
|
|
# Activity settings
|
|
activity_group = DarkGroupBox("⚙️ Activity Settings")
|
|
activity_layout = QGridLayout(activity_group)
|
|
activity_layout.addWidget(QLabel("Shots/Hour:"), 0, 0)
|
|
activity_layout.addWidget(self.shots_per_hour_spin, 0, 1)
|
|
activity_layout.addWidget(QLabel("Hits Taken/Hour:"), 0, 2)
|
|
activity_layout.addWidget(self.hits_per_hour_spin, 0, 3)
|
|
activity_layout.addWidget(QLabel("Heals/Hour:"), 0, 4)
|
|
activity_layout.addWidget(self.heals_per_hour_spin, 0, 5)
|
|
right_layout.addWidget(activity_group)
|
|
|
|
# Weapon configuration
|
|
weapon_layout = QFormLayout(self.weapon_group)
|
|
|
|
weapon_select_layout = QHBoxLayout()
|
|
weapon_select_layout.addWidget(self.select_weapon_btn)
|
|
weapon_select_layout.addWidget(self.weapon_name_label, stretch=1)
|
|
weapon_layout.addRow("Weapon:", weapon_select_layout)
|
|
|
|
weapon_layout.addRow("Damage:", self.weapon_damage_edit)
|
|
weapon_layout.addRow("Decay/shot (PEC):", self.weapon_decay_edit)
|
|
weapon_layout.addRow("Ammo/shot (PEC):", self.weapon_ammo_edit)
|
|
weapon_layout.addRow("Total DPP:", self.dpp_label)
|
|
|
|
# Attachments
|
|
attachments_frame = QFrame()
|
|
attachments_layout = QGridLayout(attachments_frame)
|
|
attachments_layout.addWidget(QLabel("Amplifier:"), 0, 0)
|
|
attachments_layout.addWidget(self.amp_label, 0, 1)
|
|
attachments_layout.addWidget(self.attach_amp_btn, 0, 2)
|
|
attachments_layout.addWidget(self.remove_amp_btn, 0, 3)
|
|
|
|
attachments_layout.addWidget(QLabel("Scope:"), 1, 0)
|
|
attachments_layout.addWidget(self.scope_label, 1, 1)
|
|
attachments_layout.addWidget(self.attach_scope_btn, 1, 2)
|
|
attachments_layout.addWidget(self.remove_scope_btn, 1, 3)
|
|
|
|
attachments_layout.addWidget(QLabel("Absorber:"), 2, 0)
|
|
attachments_layout.addWidget(self.absorber_label, 2, 1)
|
|
attachments_layout.addWidget(self.attach_absorber_btn, 2, 2)
|
|
attachments_layout.addWidget(self.remove_absorber_btn, 2, 3)
|
|
|
|
weapon_layout.addRow("Attachments:", attachments_frame)
|
|
right_layout.addWidget(self.weapon_group)
|
|
|
|
# Armor configuration - COMPLETE SYSTEM
|
|
armor_layout = QVBoxLayout(self.armor_group)
|
|
|
|
# Armor set selection row
|
|
set_layout = QHBoxLayout()
|
|
set_layout.addWidget(QLabel("<b>Armor Set:</b>"))
|
|
set_layout.addWidget(self.armor_set_combo, stretch=1)
|
|
set_layout.addWidget(self.equip_set_btn)
|
|
set_layout.addWidget(self.clear_armor_btn)
|
|
armor_layout.addLayout(set_layout)
|
|
|
|
# Add API armor selector button
|
|
self.select_armor_api_btn = QPushButton("🔍 Search Entropia Nexus Armors")
|
|
self.select_armor_api_btn.setObjectName("selectButton")
|
|
self.select_armor_api_btn.clicked.connect(self._on_select_armor_from_api)
|
|
armor_layout.addWidget(self.select_armor_api_btn)
|
|
|
|
# Armor summary
|
|
armor_layout.addWidget(self.armor_summary_label)
|
|
|
|
# Separator
|
|
separator = QFrame()
|
|
separator.setFrameShape(QFrame.Shape.HLine)
|
|
separator.setStyleSheet("background-color: #3d3d3d;")
|
|
separator.setFixedHeight(2)
|
|
armor_layout.addWidget(separator)
|
|
|
|
# Individual slot widgets
|
|
slots_label = QLabel("<b>Individual Pieces & Plates:</b>")
|
|
slots_label.setStyleSheet("padding-top: 10px;")
|
|
armor_layout.addWidget(slots_label)
|
|
|
|
for slot in ALL_ARMOR_SLOTS:
|
|
armor_layout.addWidget(self.slot_widgets[slot])
|
|
|
|
right_layout.addWidget(self.armor_group)
|
|
|
|
# Healing configuration
|
|
heal_layout = QFormLayout(self.heal_group)
|
|
|
|
# Add healing tool search button
|
|
self.select_healing_api_btn = QPushButton("🔍 Search Healing Tools from Nexus")
|
|
self.select_healing_api_btn.setObjectName("selectButton")
|
|
self.select_healing_api_btn.clicked.connect(self._on_select_healing_from_api)
|
|
heal_layout.addRow(self.select_healing_api_btn)
|
|
|
|
heal_layout.addRow("Healing Tool:", self.heal_combo)
|
|
heal_layout.addRow("Cost/heal (PEC):", self.heal_cost_edit)
|
|
heal_layout.addRow("Heal amount:", self.heal_amount_edit)
|
|
right_layout.addWidget(self.heal_group)
|
|
|
|
# Cost summary
|
|
summary_layout = QFormLayout(self.summary_group)
|
|
summary_layout.addRow("Weapon Cost:", self.weapon_cost_label)
|
|
summary_layout.addRow("Armor Cost:", self.armor_cost_label)
|
|
summary_layout.addRow("Healing Cost:", self.heal_cost_label)
|
|
summary_layout.addRow("Total DPP:", self.total_dpp_label)
|
|
summary_layout.addRow("Total Cost:", self.total_cost_label)
|
|
|
|
# Protection summary
|
|
summary_layout.addRow("Protection:", self.protection_summary_label)
|
|
|
|
break_even_layout = QHBoxLayout()
|
|
break_even_layout.addWidget(QLabel("Mob Health:"))
|
|
break_even_layout.addWidget(self.mob_health_edit)
|
|
break_even_layout.addWidget(self.calc_break_even_btn)
|
|
summary_layout.addRow("Break-Even:", break_even_layout)
|
|
summary_layout.addRow("", self.break_even_label)
|
|
|
|
right_layout.addWidget(self.summary_group)
|
|
|
|
# Save button
|
|
right_layout.addWidget(self.save_btn)
|
|
|
|
right_layout.addStretch()
|
|
right_scroll.setWidget(right_widget)
|
|
|
|
# Splitter
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
splitter.addWidget(left_panel)
|
|
splitter.addWidget(right_scroll)
|
|
splitter.setSizes([250, 850])
|
|
|
|
main_layout.addWidget(splitter)
|
|
|
|
def _connect_signals(self):
|
|
"""Connect all signal handlers."""
|
|
# Weapon selection
|
|
self.select_weapon_btn.clicked.connect(self._on_select_weapon)
|
|
self.weapon_damage_edit.textChanged.connect(self._update_calculations)
|
|
self.weapon_decay_edit.textChanged.connect(self._update_calculations)
|
|
self.weapon_ammo_edit.textChanged.connect(self._update_calculations)
|
|
|
|
# Attachments
|
|
self.attach_amp_btn.clicked.connect(lambda: self._on_attach("amplifier"))
|
|
self.attach_scope_btn.clicked.connect(lambda: self._on_attach("scope"))
|
|
self.attach_absorber_btn.clicked.connect(lambda: self._on_attach("absorber"))
|
|
self.remove_amp_btn.clicked.connect(self._on_remove_amp)
|
|
self.remove_scope_btn.clicked.connect(self._on_remove_scope)
|
|
self.remove_absorber_btn.clicked.connect(self._on_remove_absorber)
|
|
|
|
# Armor
|
|
self.equip_set_btn.clicked.connect(self._on_equip_full_set)
|
|
self.clear_armor_btn.clicked.connect(self._on_clear_armor)
|
|
|
|
# Healing
|
|
self.heal_combo.currentTextChanged.connect(self._on_heal_changed)
|
|
|
|
# Activity settings
|
|
self.shots_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
self.hits_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
self.heals_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
|
|
# Buttons
|
|
self.save_btn.clicked.connect(self._save_loadout)
|
|
self.load_btn.clicked.connect(self._load_selected)
|
|
self.delete_btn.clicked.connect(self._delete_selected)
|
|
self.new_btn.clicked.connect(self._new_loadout)
|
|
self.refresh_btn.clicked.connect(self._load_saved_loadouts)
|
|
self.close_btn.clicked.connect(self.reject)
|
|
self.calc_break_even_btn.clicked.connect(self._calculate_break_even)
|
|
|
|
# Double click on list
|
|
self.saved_list.itemDoubleClicked.connect(self._load_from_item)
|
|
|
|
def _populate_armor_sets(self):
|
|
"""Populate armor set combo."""
|
|
self.armor_set_combo.clear()
|
|
self.armor_set_combo.addItem("-- Select a Set --")
|
|
|
|
sets = get_all_armor_sets()
|
|
for armor_set in sets:
|
|
total_prot = armor_set.get_total_protection().get_total()
|
|
display = f"{armor_set.name} (Prot: {total_prot})"
|
|
self.armor_set_combo.addItem(display, armor_set)
|
|
|
|
def _populate_healing_data(self):
|
|
"""Populate healing combo with real data from database."""
|
|
self.heal_combo.clear()
|
|
self.heal_combo.addItem("-- Custom --")
|
|
|
|
# Get real healing tools
|
|
healing_tools = get_healing_tools_data()
|
|
|
|
# Sort by category (chips last)
|
|
medical_tools = [h for h in healing_tools if not h.get("is_chip", False)]
|
|
chips = [h for h in healing_tools if h.get("is_chip", False)]
|
|
|
|
# Add medical tools first
|
|
if medical_tools:
|
|
self.heal_combo.addItem("--- Medical Tools ---")
|
|
for tool in medical_tools:
|
|
self.heal_combo.addItem(tool["name"])
|
|
|
|
# Add restoration chips
|
|
if chips:
|
|
self.heal_combo.addItem("--- Restoration Chips ---")
|
|
for chip in sorted(chips, key=lambda x: x["amount"]):
|
|
self.heal_combo.addItem(chip["name"])
|
|
|
|
def _on_select_weapon(self):
|
|
"""Open weapon selector dialog."""
|
|
dialog = WeaponSelectorDialog(self)
|
|
dialog.weapon_selected.connect(self._on_weapon_selected)
|
|
dialog.exec()
|
|
|
|
def _on_weapon_selected(self, weapon: WeaponStats):
|
|
"""Handle weapon selection."""
|
|
self.current_weapon = weapon
|
|
self.weapon_name_label.setText(weapon.name)
|
|
self.weapon_damage_edit.set_decimal(weapon.total_damage)
|
|
self.weapon_decay_edit.set_decimal(weapon.decay or Decimal("0"))
|
|
self.weapon_ammo_edit.set_decimal(Decimal(weapon.ammo_burn or 0))
|
|
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)
|
|
dialog.armor_selected.connect(self._on_api_armor_selected)
|
|
dialog.exec()
|
|
|
|
def _on_api_armor_selected(self, armor: NexusArmor):
|
|
"""Handle armor selection from API."""
|
|
# Store selected armor info
|
|
self._selected_api_armor = armor
|
|
QMessageBox.information(
|
|
self,
|
|
"Armor Selected",
|
|
f"Selected: {armor.name}\n"
|
|
f"Durability: {armor.durability}\n"
|
|
f"Protection: Impact {armor.protection_impact}, Cut {armor.protection_cut}, Stab {armor.protection_stab}"
|
|
)
|
|
|
|
def _on_select_healing_from_api(self):
|
|
"""Open healing tool selector dialog from Nexus API."""
|
|
from ui.healing_selector import HealingSelectorDialog
|
|
dialog = HealingSelectorDialog(self)
|
|
dialog.tool_selected.connect(self._on_api_healing_selected)
|
|
dialog.exec()
|
|
|
|
def _on_api_healing_selected(self, tool: NexusHealingTool):
|
|
"""Handle healing tool selection from API."""
|
|
self._selected_api_healing = tool
|
|
|
|
# Update the healing combo to show selected tool
|
|
# Find or add the tool to combo
|
|
index = self.heal_combo.findText(tool.name)
|
|
if index < 0:
|
|
self.heal_combo.addItem(tool.name)
|
|
index = self.heal_combo.count() - 1
|
|
self.heal_combo.setCurrentIndex(index)
|
|
|
|
# Update cost and amount fields
|
|
self.heal_cost_edit.setText(str(tool.decay))
|
|
self.heal_amount_edit.setText(str(tool.heal_amount))
|
|
|
|
self._update_calculations()
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Healing Tool Selected",
|
|
f"Selected: {tool.name}\n"
|
|
f"Heal: {tool.heal_amount} HP\n"
|
|
f"Decay: {tool.decay:.2f} PEC ({tool.heal_per_pec:.2f} hp/pec)"
|
|
)
|
|
|
|
def _on_attach(self, attachment_type: str):
|
|
"""Handle attachment selection."""
|
|
from core.attachments import get_mock_attachments
|
|
|
|
attachments = get_mock_attachments(attachment_type)
|
|
if not attachments:
|
|
QMessageBox.information(self, "No Attachments", f"No {attachment_type} attachments available.")
|
|
return
|
|
|
|
# Create simple selection dialog
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle(f"Select {attachment_type.title()}")
|
|
dialog.setMinimumWidth(400)
|
|
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
list_widget = QListWidget()
|
|
for att in attachments:
|
|
item = QListWidgetItem(f"📎 {att.name}")
|
|
item.setData(Qt.ItemDataRole.UserRole, att)
|
|
list_widget.addItem(item)
|
|
|
|
layout.addWidget(list_widget)
|
|
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
# Add None option
|
|
none_btn = QPushButton("Remove Attachment")
|
|
none_btn.clicked.connect(lambda: self._clear_attachment(attachment_type) or dialog.reject())
|
|
layout.addWidget(none_btn)
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
selected = list_widget.currentItem()
|
|
if selected:
|
|
att = selected.data(Qt.ItemDataRole.UserRole)
|
|
self._apply_attachment(attachment_type, att)
|
|
|
|
def _apply_attachment(self, attachment_type: str, att):
|
|
"""Apply selected attachment."""
|
|
if attachment_type == "amplifier":
|
|
self.amp_label.setText(f"{att.name} (+{att.damage_increase} dmg)")
|
|
elif attachment_type == "scope":
|
|
self.scope_label.setText(f"{att.name} (+{att.range_increase}m)")
|
|
elif attachment_type == "absorber":
|
|
self.absorber_label.setText(f"{att.name} (-{att.damage_reduction} dmg)")
|
|
|
|
self._update_calculations()
|
|
|
|
def _clear_attachment(self, attachment_type: str):
|
|
"""Clear an attachment."""
|
|
if attachment_type == "amplifier":
|
|
self.amp_label.setText("None")
|
|
elif attachment_type == "scope":
|
|
self.scope_label.setText("None")
|
|
elif attachment_type == "absorber":
|
|
self.absorber_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_amp(self):
|
|
"""Remove amplifier."""
|
|
self.amp_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_scope(self):
|
|
"""Remove scope."""
|
|
self.scope_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_absorber(self):
|
|
"""Remove absorber."""
|
|
self.absorber_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_equip_full_set(self):
|
|
"""Equip a full armor set."""
|
|
if self.armor_set_combo.currentIndex() <= 0:
|
|
QMessageBox.information(self, "No Selection", "Please select an armor set first.")
|
|
return
|
|
|
|
armor_set = self.armor_set_combo.currentData()
|
|
if not armor_set:
|
|
return
|
|
|
|
# Clear any individual pieces
|
|
for widget in self.slot_widgets.values():
|
|
widget.set_piece(None)
|
|
widget.set_plate(None)
|
|
|
|
# Equip set pieces
|
|
for slot, piece in armor_set.pieces.items():
|
|
if slot in self.slot_widgets:
|
|
self.slot_widgets[slot].set_piece(piece)
|
|
|
|
self.current_armor_set = armor_set
|
|
self._update_armor_summary()
|
|
self._update_calculations()
|
|
|
|
QMessageBox.information(self, "Set Equipped", f"Equipped {armor_set.name}")
|
|
|
|
def _on_clear_armor(self):
|
|
"""Clear all armor."""
|
|
for widget in self.slot_widgets.values():
|
|
widget.set_piece(None)
|
|
widget.set_plate(None)
|
|
|
|
self.current_armor_set = None
|
|
self.armor_set_combo.setCurrentIndex(0)
|
|
self._update_armor_summary()
|
|
self._update_calculations()
|
|
|
|
def _on_armor_changed(self):
|
|
"""Handle armor piece or plate change."""
|
|
# If individual pieces are changed, we're no longer using a pure full set
|
|
if self.current_armor_set:
|
|
# Check if all pieces match the set
|
|
all_match = True
|
|
for slot, piece in self.current_armor_set.pieces.items():
|
|
widget = self.slot_widgets.get(slot)
|
|
if widget:
|
|
current = widget.get_piece()
|
|
if not current or current.item_id != piece.item_id:
|
|
all_match = False
|
|
break
|
|
|
|
if not all_match:
|
|
self.current_armor_set = None
|
|
|
|
self._update_armor_summary()
|
|
self._update_calculations()
|
|
|
|
def _update_armor_summary(self):
|
|
"""Update armor summary display."""
|
|
equipped_count = 0
|
|
for widget in self.slot_widgets.values():
|
|
if widget.get_piece():
|
|
equipped_count += 1
|
|
|
|
if equipped_count == 0:
|
|
self.armor_summary_label.setText("No armor equipped")
|
|
self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;")
|
|
elif equipped_count == 7:
|
|
if self.current_armor_set:
|
|
self.armor_summary_label.setText(f"✓ Full Set: {self.current_armor_set.name}")
|
|
self.armor_summary_label.setStyleSheet("color: #4caf50; font-weight: bold; padding: 5px;")
|
|
else:
|
|
self.armor_summary_label.setText(f"✓ 7/7 pieces equipped (Mixed Set)")
|
|
self.armor_summary_label.setStyleSheet("color: #4caf50; padding: 5px;")
|
|
else:
|
|
self.armor_summary_label.setText(f"⚠ {equipped_count}/7 pieces equipped")
|
|
self.armor_summary_label.setStyleSheet("color: #ff9800; padding: 5px;")
|
|
|
|
def _on_heal_changed(self, name: str):
|
|
"""Handle healing selection change."""
|
|
if name == "-- Custom --":
|
|
self.heal_cost_edit.setEnabled(True)
|
|
self.heal_amount_edit.setEnabled(True)
|
|
self.heal_cost_edit.clear()
|
|
self.heal_amount_edit.clear()
|
|
else:
|
|
for heal in MOCK_HEALING:
|
|
if heal["name"] == name:
|
|
self.heal_cost_edit.set_decimal(heal["cost"])
|
|
self.heal_amount_edit.set_decimal(heal["amount"])
|
|
break
|
|
self.heal_cost_edit.setEnabled(False)
|
|
self.heal_amount_edit.setEnabled(False)
|
|
self._update_calculations()
|
|
|
|
def _update_calculations(self):
|
|
"""Update all cost and DPP calculations."""
|
|
try:
|
|
config = self._get_current_config()
|
|
|
|
# Update DPP
|
|
dpp = config.calculate_dpp()
|
|
self.dpp_label.setText(f"{dpp:.4f}")
|
|
self.total_dpp_label.setText(f"{dpp:.4f}")
|
|
|
|
# Update cost breakdown
|
|
weapon_cost = config.calculate_weapon_cost_per_hour()
|
|
armor_cost = config.calculate_armor_cost_per_hour()
|
|
heal_cost = config.calculate_heal_cost_per_hour()
|
|
total_cost = config.calculate_total_cost_per_hour()
|
|
|
|
self.weapon_cost_label.setText(f"{weapon_cost:.0f} PEC/hr")
|
|
self.armor_cost_label.setText(f"{armor_cost:.0f} PEC/hr")
|
|
self.heal_cost_label.setText(f"{heal_cost:.0f} PEC/hr")
|
|
self.total_cost_label.setText(f"{total_cost:.2f} PED/hr")
|
|
|
|
# Update protection summary
|
|
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()} | {prot_text}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Calculation error: {e}")
|
|
|
|
def _calculate_break_even(self):
|
|
"""Calculate and display break-even loot value."""
|
|
try:
|
|
config = self._get_current_config()
|
|
mob_health = self.mob_health_edit.get_decimal()
|
|
|
|
if mob_health <= 0:
|
|
QMessageBox.warning(self, "Invalid Input", "Mob health must be greater than 0")
|
|
return
|
|
|
|
break_even = config.calculate_break_even(mob_health)
|
|
self.break_even_label.setText(
|
|
f"Break-even: {break_even:.2f} PED (mob HP: {mob_health})"
|
|
)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Calculation failed: {str(e)}")
|
|
|
|
def _get_current_config(self) -> LoadoutConfig:
|
|
"""Get current configuration from UI fields."""
|
|
# Build equipped armor from slot widgets
|
|
equipped = EquippedArmor()
|
|
for slot, widget in self.slot_widgets.items():
|
|
piece = widget.get_piece()
|
|
if piece:
|
|
# Create a copy
|
|
piece_copy = ArmorPiece(
|
|
name=piece.name,
|
|
item_id=piece.item_id,
|
|
slot=piece.slot,
|
|
set_name=piece.set_name,
|
|
decay_per_hit=piece.decay_per_hit,
|
|
protection=ProtectionProfile(
|
|
stab=piece.protection.stab,
|
|
cut=piece.protection.cut,
|
|
impact=piece.protection.impact,
|
|
penetration=piece.protection.penetration,
|
|
shrapnel=piece.protection.shrapnel,
|
|
burn=piece.protection.burn,
|
|
cold=piece.protection.cold,
|
|
acid=piece.protection.acid,
|
|
electric=piece.protection.electric,
|
|
),
|
|
durability=piece.durability,
|
|
weight=piece.weight,
|
|
)
|
|
|
|
# Attach plate if selected
|
|
plate = widget.get_plate()
|
|
if plate:
|
|
plate_copy = ArmorPlate(
|
|
name=plate.name,
|
|
item_id=plate.item_id,
|
|
decay_per_hit=plate.decay_per_hit,
|
|
protection=ProtectionProfile(
|
|
stab=plate.protection.stab,
|
|
cut=plate.protection.cut,
|
|
impact=plate.protection.impact,
|
|
penetration=plate.protection.penetration,
|
|
shrapnel=plate.protection.shrapnel,
|
|
burn=plate.protection.burn,
|
|
cold=plate.protection.cold,
|
|
acid=plate.protection.acid,
|
|
electric=plate.protection.electric,
|
|
),
|
|
durability=plate.durability,
|
|
)
|
|
piece_copy.attach_plate(plate_copy)
|
|
|
|
equipped.equip_piece(piece_copy)
|
|
|
|
# Set full set if all pieces match
|
|
if self.current_armor_set:
|
|
equipped.equip_full_set(self.current_armor_set)
|
|
|
|
return LoadoutConfig(
|
|
name=self.loadout_name_edit.text().strip() or "Unnamed",
|
|
weapon_name=self.current_weapon.name if self.current_weapon else (self.weapon_name_label.text() if self.weapon_name_label.text() != "No weapon selected" else "-- Custom --"),
|
|
weapon_id=self.current_weapon.id if self.current_weapon else 0,
|
|
weapon_damage=self.weapon_damage_edit.get_decimal(),
|
|
weapon_decay_pec=self.weapon_decay_edit.get_decimal(),
|
|
weapon_ammo_pec=self.weapon_ammo_edit.get_decimal(),
|
|
equipped_armor=equipped if equipped.get_all_pieces() else None,
|
|
armor_set_name=self.current_armor_set.name if self.current_armor_set else "-- Mixed --",
|
|
heal_name=self.heal_combo.currentText(),
|
|
heal_cost_pec=self.heal_cost_edit.get_decimal(),
|
|
heal_amount=self.heal_amount_edit.get_decimal(),
|
|
shots_per_hour=self.shots_per_hour_spin.value(),
|
|
hits_per_hour=self.hits_per_hour_spin.value(),
|
|
heals_per_hour=self.heals_per_hour_spin.value(),
|
|
)
|
|
|
|
def _set_config(self, config: LoadoutConfig):
|
|
"""Set UI fields from configuration."""
|
|
self.loadout_name_edit.setText(config.name)
|
|
self.shots_per_hour_spin.setValue(config.shots_per_hour)
|
|
self.hits_per_hour_spin.setValue(config.hits_per_hour)
|
|
self.heals_per_hour_spin.setValue(config.heals_per_hour)
|
|
|
|
# Weapon
|
|
self.weapon_name_label.setText(config.weapon_name)
|
|
self.weapon_damage_edit.set_decimal(config.weapon_damage)
|
|
self.weapon_decay_edit.set_decimal(config.weapon_decay_pec)
|
|
self.weapon_ammo_edit.set_decimal(config.weapon_ammo_pec)
|
|
|
|
# Weapon attachments (simplified - just labels)
|
|
self.amp_label.setText("None")
|
|
self.scope_label.setText("None")
|
|
self.absorber_label.setText("None")
|
|
|
|
# Armor - use equipped_armor if available
|
|
if config.equipped_armor:
|
|
self.equipped_armor = config.equipped_armor
|
|
pieces = config.equipped_armor.get_all_pieces()
|
|
|
|
for slot, widget in self.slot_widgets.items():
|
|
piece = pieces.get(slot)
|
|
widget.set_piece(piece)
|
|
if piece and piece.attached_plate:
|
|
widget.set_plate(piece.attached_plate)
|
|
else:
|
|
widget.set_plate(None)
|
|
|
|
# Check if it's a full set
|
|
if config.equipped_armor.full_set:
|
|
self.current_armor_set = config.equipped_armor.full_set
|
|
# Select in combo
|
|
for i in range(self.armor_set_combo.count()):
|
|
data = self.armor_set_combo.itemData(i)
|
|
if data and data.set_id == self.current_armor_set.set_id:
|
|
self.armor_set_combo.setCurrentIndex(i)
|
|
break
|
|
else:
|
|
self.current_armor_set = None
|
|
self.armor_set_combo.setCurrentIndex(0)
|
|
else:
|
|
# Legacy or empty
|
|
self._on_clear_armor()
|
|
|
|
self._update_armor_summary()
|
|
|
|
# Healing
|
|
self.heal_combo.setCurrentText(config.heal_name)
|
|
self.heal_cost_edit.set_decimal(config.heal_cost_pec)
|
|
self.heal_amount_edit.set_decimal(config.heal_amount)
|
|
|
|
# Store config
|
|
self.current_loadout = config
|
|
|
|
self._update_calculations()
|
|
|
|
def _save_loadout(self):
|
|
"""Save current loadout to file."""
|
|
name = self.loadout_name_edit.text().strip()
|
|
if not name:
|
|
QMessageBox.warning(self, "Missing Name", "Please enter a loadout name")
|
|
return
|
|
|
|
safe_name = "".join(c for c in name if c.isalnum() or c in "._- ").strip()
|
|
if not safe_name:
|
|
safe_name = "unnamed"
|
|
|
|
config = self._get_current_config()
|
|
config.name = name
|
|
|
|
filepath = self.config_dir / f"{safe_name}.json"
|
|
|
|
try:
|
|
with open(filepath, 'w') as f:
|
|
json.dump(config.to_dict(), f, indent=2)
|
|
|
|
self.current_loadout = config
|
|
self.loadout_saved.emit(name)
|
|
self._load_saved_loadouts()
|
|
|
|
QMessageBox.information(self, "Saved", f"Loadout '{name}' saved successfully!")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save: {str(e)}")
|
|
|
|
def _load_saved_loadouts(self):
|
|
"""Load list of saved loadouts."""
|
|
self.saved_list.clear()
|
|
|
|
try:
|
|
for filepath in sorted(self.config_dir.glob("*.json")):
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
data = json.load(f)
|
|
config = LoadoutConfig.from_dict(data)
|
|
|
|
item = QListWidgetItem(f"📋 {config.name}")
|
|
item.setData(Qt.ItemDataRole.UserRole, str(filepath))
|
|
|
|
# Build tooltip
|
|
dpp = config.calculate_dpp()
|
|
cost = config.calculate_total_cost_per_hour()
|
|
tooltip = (
|
|
f"Weapon: {config.weapon_name}\n"
|
|
f"Armor: {config.armor_set_name}\n"
|
|
f"Total DPP: {dpp:.3f}\n"
|
|
f"Cost/hr: {cost:.2f} PED"
|
|
)
|
|
item.setToolTip(tooltip)
|
|
self.saved_list.addItem(item)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load {filepath}: {e}")
|
|
continue
|
|
except Exception as e:
|
|
logger.error(f"Failed to list loadouts: {e}")
|
|
|
|
def _load_selected(self):
|
|
"""Load the selected loadout from the list."""
|
|
item = self.saved_list.currentItem()
|
|
if item:
|
|
self._load_from_item(item)
|
|
else:
|
|
QMessageBox.information(self, "No Selection", "Please select a loadout to load")
|
|
|
|
def _load_from_item(self, item: QListWidgetItem):
|
|
"""Load loadout from a list item."""
|
|
filepath = item.data(Qt.ItemDataRole.UserRole)
|
|
if not filepath:
|
|
return
|
|
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
data = json.load(f)
|
|
config = LoadoutConfig.from_dict(data)
|
|
|
|
self._set_config(config)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
|
|
|
|
def _delete_selected(self):
|
|
"""Delete the selected loadout."""
|
|
item = self.saved_list.currentItem()
|
|
if not item:
|
|
QMessageBox.information(self, "No Selection", "Please select a loadout to delete")
|
|
return
|
|
|
|
filepath = item.data(Qt.ItemDataRole.UserRole)
|
|
name = item.text().replace("📋 ", "")
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Confirm Delete",
|
|
f"Are you sure you want to delete '{name}'?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
try:
|
|
os.remove(filepath)
|
|
self._load_saved_loadouts()
|
|
QMessageBox.information(self, "Deleted", f"'{name}' deleted successfully")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to delete: {str(e)}")
|
|
|
|
def _new_loadout(self):
|
|
"""Clear all fields for a new loadout."""
|
|
self.loadout_name_edit.clear()
|
|
self.weapon_name_label.setText("No weapon selected")
|
|
|
|
# Clear weapon
|
|
self.weapon_damage_edit.clear()
|
|
self.weapon_decay_edit.clear()
|
|
self.weapon_ammo_edit.clear()
|
|
|
|
# Clear attachments
|
|
self.amp_label.setText("None")
|
|
self.scope_label.setText("None")
|
|
self.absorber_label.setText("None")
|
|
|
|
# Clear armor
|
|
self._on_clear_armor()
|
|
|
|
# Clear healing
|
|
self.heal_cost_edit.clear()
|
|
self.heal_amount_edit.clear()
|
|
|
|
# Reset values
|
|
self.shots_per_hour_spin.setValue(3600)
|
|
self.hits_per_hour_spin.setValue(720)
|
|
self.heals_per_hour_spin.setValue(60)
|
|
self.mob_health_edit.set_decimal(Decimal("100"))
|
|
|
|
# Reset combos
|
|
self.heal_combo.setCurrentIndex(0)
|
|
|
|
# Clear stored objects
|
|
self.current_weapon = None
|
|
self.current_armor_set = None
|
|
self.current_loadout = None
|
|
|
|
self._update_calculations()
|
|
|
|
def get_current_loadout(self) -> Optional[LoadoutConfig]:
|
|
"""Get the currently loaded/created loadout."""
|
|
return self.current_loadout
|
|
|
|
|
|
# ============================================================================
|
|
# Main entry point for testing
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Run the loadout manager as a standalone application."""
|
|
import sys
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
app = QApplication(sys.argv)
|
|
app.setStyle('Fusion')
|
|
|
|
# Set application-wide font
|
|
font = QFont("Segoe UI", 10)
|
|
app.setFont(font)
|
|
|
|
dialog = LoadoutManagerDialog()
|
|
|
|
# Connect signal for testing
|
|
dialog.loadout_saved.connect(lambda name: print(f"Loadout saved: {name}"))
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
config = dialog.get_current_loadout()
|
|
if config:
|
|
print(f"\nFinal Loadout: {config.name}")
|
|
print(f" Weapon: {config.weapon_name}")
|
|
print(f" Armor: {config.armor_set_name}")
|
|
if config.equipped_armor:
|
|
pieces = config.equipped_armor.get_all_pieces()
|
|
print(f" Armor Pieces: {len(pieces)}/7")
|
|
print(f" Total DPP: {config.calculate_dpp():.4f}")
|
|
print(f" Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
|
|
print(f" Protection: {format_protection(config.get_total_protection())}")
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|