1045 lines
42 KiB
Python
1045 lines
42 KiB
Python
"""
|
|
Lemontropia Suite - Loadout Manager UI v5.0
|
|
Full gear support with weapon amplifiers, mindforce implants, and armor platings.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
|
QLineEdit, QLabel, QPushButton, QGroupBox,
|
|
QMessageBox, QListWidget, QListWidgetItem,
|
|
QSplitter, QWidget, QFrame, QGridLayout, QScrollArea,
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal
|
|
|
|
from core.nexus_full_api import get_nexus_api, NexusWeapon
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Full Gear Loadout Config
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class LoadoutConfig:
|
|
"""Complete loadout configuration with full gear support.
|
|
|
|
Core principle: Store all gear types needed for comprehensive cost tracking.
|
|
"""
|
|
# Identity
|
|
name: str = "Unnamed"
|
|
version: int = 3 # Version 3 = full gear support
|
|
|
|
# === WEAPON & ATTACHMENTS ===
|
|
weapon_cost_per_shot: Decimal = Decimal("0")
|
|
weapon_name: str = "None"
|
|
weapon_damage: Decimal = Decimal("0")
|
|
weapon_decay_pec: Decimal = Decimal("0")
|
|
weapon_ammo_pec: Decimal = Decimal("0")
|
|
weapon_api_id: Optional[int] = None
|
|
|
|
# Weapon Amplifier
|
|
weapon_amp_id: Optional[int] = None
|
|
weapon_amp_name: str = "None"
|
|
weapon_amp_decay: Decimal = Decimal("0")
|
|
weapon_amp_damage_bonus: Decimal = Decimal("0")
|
|
|
|
# === ARMOR & PLATINGS ===
|
|
armor_cost_per_hit: Decimal = Decimal("0")
|
|
armor_name: str = "None"
|
|
armor_decay_pec: Decimal = Decimal("0")
|
|
armor_api_id: Optional[int] = None
|
|
|
|
# Armor Plating
|
|
plating_id: Optional[int] = None
|
|
plating_name: str = "None"
|
|
plating_decay: Decimal = Decimal("0")
|
|
plating_protection_summary: str = ""
|
|
|
|
# === HEALING & MINDFORCE ===
|
|
healing_cost_per_heal: Decimal = Decimal("0")
|
|
healing_name: str = "None"
|
|
healing_decay_pec: Decimal = Decimal("0")
|
|
healing_api_id: Optional[int] = None
|
|
|
|
# Mindforce Implant (for healing chips)
|
|
mindforce_implant_id: Optional[int] = None
|
|
mindforce_implant_name: str = "None"
|
|
mindforce_implant_decay: Decimal = Decimal("0")
|
|
mindforce_implant_heal_amount: Decimal = Decimal("0")
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Serialize to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'version': self.version,
|
|
# Weapon
|
|
'weapon_cost_per_shot': str(self.weapon_cost_per_shot),
|
|
'weapon_name': self.weapon_name,
|
|
'weapon_damage': str(self.weapon_damage),
|
|
'weapon_decay_pec': str(self.weapon_decay_pec),
|
|
'weapon_ammo_pec': str(self.weapon_ammo_pec),
|
|
'weapon_api_id': self.weapon_api_id,
|
|
# Weapon Amplifier
|
|
'weapon_amp_id': self.weapon_amp_id,
|
|
'weapon_amp_name': self.weapon_amp_name,
|
|
'weapon_amp_decay': str(self.weapon_amp_decay),
|
|
'weapon_amp_damage_bonus': str(self.weapon_amp_damage_bonus),
|
|
# Armor
|
|
'armor_cost_per_hit': str(self.armor_cost_per_hit),
|
|
'armor_name': self.armor_name,
|
|
'armor_decay_pec': str(self.armor_decay_pec),
|
|
'armor_api_id': self.armor_api_id,
|
|
# Plating
|
|
'plating_id': self.plating_id,
|
|
'plating_name': self.plating_name,
|
|
'plating_decay': str(self.plating_decay),
|
|
'plating_protection_summary': self.plating_protection_summary,
|
|
# Healing
|
|
'healing_cost_per_heal': str(self.healing_cost_per_heal),
|
|
'healing_name': self.healing_name,
|
|
'healing_decay_pec': str(self.healing_decay_pec),
|
|
'healing_api_id': self.healing_api_id,
|
|
# Mindforce
|
|
'mindforce_implant_id': self.mindforce_implant_id,
|
|
'mindforce_implant_name': self.mindforce_implant_name,
|
|
'mindforce_implant_decay': str(self.mindforce_implant_decay),
|
|
'mindforce_implant_heal_amount': str(self.mindforce_implant_heal_amount),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "LoadoutConfig":
|
|
"""Deserialize from dictionary with legacy support."""
|
|
version = data.get('version', 1)
|
|
|
|
if version == 1:
|
|
return cls._from_legacy_v1(data)
|
|
elif version == 2:
|
|
return cls._from_v2(data)
|
|
else:
|
|
return cls._from_v3(data)
|
|
|
|
@classmethod
|
|
def _from_v3(cls, data: dict) -> "LoadoutConfig":
|
|
"""Parse version 3 (current) format."""
|
|
def get_decimal(key: str, default: str = "0") -> Decimal:
|
|
try:
|
|
return Decimal(str(data.get(key, default)))
|
|
except Exception:
|
|
return Decimal(default)
|
|
|
|
return cls(
|
|
name=data.get('name', 'Unnamed'),
|
|
version=3,
|
|
# Weapon
|
|
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
|
|
weapon_name=data.get('weapon_name', 'None'),
|
|
weapon_damage=get_decimal('weapon_damage'),
|
|
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
|
weapon_ammo_pec=get_decimal('weapon_ammo_pec'),
|
|
weapon_api_id=data.get('weapon_api_id'),
|
|
# Weapon Amplifier
|
|
weapon_amp_id=data.get('weapon_amp_id'),
|
|
weapon_amp_name=data.get('weapon_amp_name', 'None'),
|
|
weapon_amp_decay=get_decimal('weapon_amp_decay'),
|
|
weapon_amp_damage_bonus=get_decimal('weapon_amp_damage_bonus'),
|
|
# Armor
|
|
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
|
|
armor_name=data.get('armor_name', 'None'),
|
|
armor_decay_pec=get_decimal('armor_decay_pec'),
|
|
armor_api_id=data.get('armor_api_id'),
|
|
# Plating
|
|
plating_id=data.get('plating_id'),
|
|
plating_name=data.get('plating_name', 'None'),
|
|
plating_decay=get_decimal('plating_decay'),
|
|
plating_protection_summary=data.get('plating_protection_summary', ''),
|
|
# Healing
|
|
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
|
|
healing_name=data.get('healing_name', 'None'),
|
|
healing_decay_pec=get_decimal('healing_decay_pec'),
|
|
healing_api_id=data.get('healing_api_id'),
|
|
# Mindforce
|
|
mindforce_implant_id=data.get('mindforce_implant_id'),
|
|
mindforce_implant_name=data.get('mindforce_implant_name', 'None'),
|
|
mindforce_implant_decay=get_decimal('mindforce_implant_decay'),
|
|
mindforce_implant_heal_amount=get_decimal('mindforce_implant_heal_amount'),
|
|
)
|
|
|
|
@classmethod
|
|
def _from_v2(cls, data: dict) -> "LoadoutConfig":
|
|
"""Convert version 2 to version 3."""
|
|
def get_decimal(key: str, default: str = "0") -> Decimal:
|
|
try:
|
|
return Decimal(str(data.get(key, default)))
|
|
except Exception:
|
|
return Decimal(default)
|
|
|
|
return cls(
|
|
name=data.get('name', 'Unnamed'),
|
|
version=3,
|
|
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
|
|
weapon_name=data.get('weapon_name', 'None'),
|
|
weapon_damage=get_decimal('weapon_damage'),
|
|
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
|
weapon_ammo_pec=get_decimal('weapon_ammo_pec'),
|
|
weapon_api_id=data.get('weapon_api_id'),
|
|
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
|
|
armor_name=data.get('armor_name', 'None'),
|
|
armor_decay_pec=get_decimal('armor_decay_pec'),
|
|
armor_api_id=data.get('armor_api_id'),
|
|
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
|
|
healing_name=data.get('healing_name', 'None'),
|
|
healing_decay_pec=get_decimal('healing_decay_pec'),
|
|
healing_api_id=data.get('healing_api_id'),
|
|
)
|
|
|
|
@classmethod
|
|
def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig":
|
|
"""Convert legacy format to new format."""
|
|
def get_decimal(key: str, default: str = "0") -> Decimal:
|
|
try:
|
|
return Decimal(str(data.get(key, default)))
|
|
except Exception:
|
|
return Decimal(default)
|
|
|
|
# Calculate costs from legacy fields
|
|
weapon_decay = get_decimal('weapon_decay_pec')
|
|
weapon_ammo = get_decimal('weapon_ammo_pec')
|
|
weapon_cost_per_shot = (weapon_decay / Decimal("100")) + (weapon_ammo * Decimal("0.0001"))
|
|
|
|
armor_decay = get_decimal('armor_decay_pec')
|
|
armor_cost_per_hit = armor_decay / Decimal("100")
|
|
|
|
heal_decay = get_decimal('heal_cost_pec')
|
|
healing_cost_per_heal = heal_decay / Decimal("100")
|
|
|
|
return cls(
|
|
name=data.get('name', 'Unnamed'),
|
|
version=3,
|
|
weapon_cost_per_shot=weapon_cost_per_shot,
|
|
weapon_name=data.get('weapon_name', data.get('weapon', 'None')),
|
|
weapon_damage=get_decimal('weapon_damage'),
|
|
weapon_decay_pec=weapon_decay,
|
|
weapon_ammo_pec=weapon_ammo,
|
|
armor_cost_per_hit=armor_cost_per_hit,
|
|
armor_name=data.get('armor_set_name', data.get('armor_name', 'None')),
|
|
armor_decay_pec=armor_decay,
|
|
healing_cost_per_heal=healing_cost_per_heal,
|
|
healing_name=data.get('heal_name', 'None'),
|
|
healing_decay_pec=heal_decay,
|
|
)
|
|
|
|
def get_total_weapon_cost_per_shot(self) -> Decimal:
|
|
"""Calculate total weapon cost including amplifier."""
|
|
base_cost = self.weapon_cost_per_shot
|
|
amp_cost = self.weapon_amp_decay / Decimal("100") # Convert PEC to PED
|
|
return base_cost + amp_cost
|
|
|
|
def get_total_healing_cost_per_heal(self) -> Decimal:
|
|
"""Calculate total healing cost including mindforce implant."""
|
|
base_cost = self.healing_cost_per_heal
|
|
implant_cost = self.mindforce_implant_decay / Decimal("100") # Convert PEC to PED
|
|
return base_cost + implant_cost
|
|
|
|
def get_total_armor_cost_per_hit(self) -> Decimal:
|
|
"""Calculate total armor cost including plating."""
|
|
base_cost = self.armor_cost_per_hit
|
|
plating_cost = self.plating_decay / Decimal("100") # Convert PEC to PED
|
|
return base_cost + plating_cost
|
|
|
|
def get_summary(self) -> Dict[str, Any]:
|
|
"""Get cost summary for display."""
|
|
return {
|
|
'name': self.name,
|
|
'weapon': self.weapon_name,
|
|
'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None",
|
|
'armor': self.armor_name,
|
|
'plating': self.plating_name if self.plating_id else "None",
|
|
'healing': self.healing_name,
|
|
'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None",
|
|
'cost_per_shot': self.get_total_weapon_cost_per_shot(),
|
|
'cost_per_hit': self.get_total_armor_cost_per_hit(),
|
|
'cost_per_heal': self.get_total_healing_cost_per_heal(),
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Full Gear Loadout Manager Dialog
|
|
# ============================================================================
|
|
|
|
class LoadoutManagerDialog(QDialog):
|
|
"""Full-featured loadout manager with all gear types."""
|
|
|
|
loadout_saved = pyqtSignal(LoadoutConfig)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Loadout Manager - Full Gear Configuration")
|
|
self.setMinimumSize(800, 700)
|
|
|
|
# State
|
|
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
|
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
self.current_config: Optional[LoadoutConfig] = None
|
|
|
|
self._setup_ui()
|
|
self._load_saved_loadouts()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup full gear UI."""
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(10)
|
|
|
|
# === Top: Loadout Name ===
|
|
name_layout = QHBoxLayout()
|
|
name_layout.addWidget(QLabel("Loadout Name:"))
|
|
self.name_edit = QLineEdit()
|
|
self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt with Dante")
|
|
name_layout.addWidget(self.name_edit)
|
|
layout.addLayout(name_layout)
|
|
|
|
# === Main Content Splitter ===
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Left: Saved Loadouts
|
|
left_widget = QWidget()
|
|
left_layout = QVBoxLayout(left_widget)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
left_layout.addWidget(QLabel("Saved Loadouts:"))
|
|
self.saved_list = QListWidget()
|
|
self.saved_list.itemClicked.connect(self._on_loadout_selected)
|
|
self.saved_list.itemDoubleClicked.connect(self._on_loadout_double_clicked)
|
|
left_layout.addWidget(self.saved_list)
|
|
|
|
btn_layout = QHBoxLayout()
|
|
self.new_btn = QPushButton("New")
|
|
self.new_btn.clicked.connect(self._new_loadout)
|
|
self.delete_btn = QPushButton("Delete")
|
|
self.delete_btn.clicked.connect(self._delete_loadout)
|
|
btn_layout.addWidget(self.new_btn)
|
|
btn_layout.addWidget(self.delete_btn)
|
|
left_layout.addLayout(btn_layout)
|
|
|
|
splitter.addWidget(left_widget)
|
|
|
|
# Right: Configuration (in a scroll area)
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
right_widget = QWidget()
|
|
right_layout = QVBoxLayout(right_widget)
|
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
right_layout.setSpacing(10)
|
|
|
|
# -- Weapon Section --
|
|
weapon_group = QGroupBox("⚔️ Weapon & Amplifier")
|
|
weapon_layout = QFormLayout(weapon_group)
|
|
|
|
self.weapon_btn = QPushButton("Select Weapon...")
|
|
self.weapon_btn.clicked.connect(self._select_weapon)
|
|
weapon_layout.addRow("Weapon:", self.weapon_btn)
|
|
|
|
self.weapon_info = QLabel("None selected")
|
|
self.weapon_info.setStyleSheet("color: #888;")
|
|
weapon_layout.addRow(self.weapon_info)
|
|
|
|
self.weapon_cost_label = QLabel("0.0000 PED")
|
|
self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
weapon_layout.addRow("Base Cost/Shot:", self.weapon_cost_label)
|
|
|
|
# Amplifier
|
|
self.amp_btn = QPushButton("Select Amplifier...")
|
|
self.amp_btn.clicked.connect(self._select_amplifier)
|
|
weapon_layout.addRow("Amplifier:", self.amp_btn)
|
|
|
|
self.amp_info = QLabel("None selected")
|
|
self.amp_info.setStyleSheet("color: #888;")
|
|
weapon_layout.addRow(self.amp_info)
|
|
|
|
self.amp_cost_label = QLabel("0.0000 PED")
|
|
self.amp_cost_label.setStyleSheet("color: #FFA07A;")
|
|
weapon_layout.addRow("Amp Cost/Shot:", self.amp_cost_label)
|
|
|
|
self.total_weapon_cost_label = QLabel("0.0000 PED")
|
|
self.total_weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
|
weapon_layout.addRow("Total Cost/Shot:", self.total_weapon_cost_label)
|
|
|
|
right_layout.addWidget(weapon_group)
|
|
|
|
# -- Armor Section --
|
|
armor_group = QGroupBox("🛡️ Armor & Plating")
|
|
armor_layout = QFormLayout(armor_group)
|
|
|
|
self.armor_btn = QPushButton("Select Armor...")
|
|
self.armor_btn.clicked.connect(self._select_armor)
|
|
armor_layout.addRow("Armor:", self.armor_btn)
|
|
|
|
self.armor_info = QLabel("None selected")
|
|
self.armor_info.setStyleSheet("color: #888;")
|
|
armor_layout.addRow(self.armor_info)
|
|
|
|
self.armor_cost_label = QLabel("0.0000 PED")
|
|
self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
armor_layout.addRow("Base Cost/Hit:", self.armor_cost_label)
|
|
|
|
# Plating
|
|
self.plating_btn = QPushButton("Select Plating...")
|
|
self.plating_btn.clicked.connect(self._select_plating)
|
|
armor_layout.addRow("Plating:", self.plating_btn)
|
|
|
|
self.plating_info = QLabel("None selected")
|
|
self.plating_info.setStyleSheet("color: #888;")
|
|
armor_layout.addRow(self.plating_info)
|
|
|
|
self.plating_cost_label = QLabel("0.0000 PED")
|
|
self.plating_cost_label.setStyleSheet("color: #FFA07A;")
|
|
armor_layout.addRow("Plating Cost/Hit:", self.plating_cost_label)
|
|
|
|
self.total_armor_cost_label = QLabel("0.0000 PED")
|
|
self.total_armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
|
armor_layout.addRow("Total Cost/Hit:", self.total_armor_cost_label)
|
|
|
|
right_layout.addWidget(armor_group)
|
|
|
|
# -- Healing Section --
|
|
healing_group = QGroupBox("💚 Healing & Mindforce")
|
|
healing_layout = QFormLayout(healing_group)
|
|
|
|
self.healing_btn = QPushButton("Select Healing Tool...")
|
|
self.healing_btn.clicked.connect(self._select_healing)
|
|
healing_layout.addRow("Healing Tool:", self.healing_btn)
|
|
|
|
self.healing_info = QLabel("None selected")
|
|
self.healing_info.setStyleSheet("color: #888;")
|
|
healing_layout.addRow(self.healing_info)
|
|
|
|
self.healing_cost_label = QLabel("0.0000 PED")
|
|
self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
healing_layout.addRow("Base Cost/Heal:", self.healing_cost_label)
|
|
|
|
# Mindforce Implant
|
|
self.mindforce_btn = QPushButton("Select Mindforce Chip...")
|
|
self.mindforce_btn.clicked.connect(self._select_mindforce)
|
|
healing_layout.addRow("Mindforce Chip:", self.mindforce_btn)
|
|
|
|
self.mindforce_info = QLabel("None selected")
|
|
self.mindforce_info.setStyleSheet("color: #888;")
|
|
healing_layout.addRow(self.mindforce_info)
|
|
|
|
self.mindforce_cost_label = QLabel("0.0000 PED")
|
|
self.mindforce_cost_label.setStyleSheet("color: #FFA07A;")
|
|
healing_layout.addRow("Chip Cost/Heal:", self.mindforce_cost_label)
|
|
|
|
self.total_healing_cost_label = QLabel("0.0000 PED")
|
|
self.total_healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
|
healing_layout.addRow("Total Cost/Heal:", self.total_healing_cost_label)
|
|
|
|
right_layout.addWidget(healing_group)
|
|
|
|
# -- Summary Section --
|
|
summary_group = QGroupBox("💰 Total Session Cost Summary")
|
|
summary_layout = QGridLayout(summary_group)
|
|
|
|
summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0)
|
|
self.summary_shot = QLabel("0.0000 PED")
|
|
self.summary_shot.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
summary_layout.addWidget(self.summary_shot, 0, 1)
|
|
|
|
summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0)
|
|
self.summary_hit = QLabel("0.0000 PED")
|
|
self.summary_hit.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
summary_layout.addWidget(self.summary_hit, 1, 1)
|
|
|
|
summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0)
|
|
self.summary_heal = QLabel("0.0000 PED")
|
|
self.summary_heal.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
|
summary_layout.addWidget(self.summary_heal, 2, 1)
|
|
|
|
summary_layout.setColumnStretch(1, 1)
|
|
right_layout.addWidget(summary_group)
|
|
|
|
right_layout.addStretch()
|
|
|
|
scroll.setWidget(right_widget)
|
|
splitter.addWidget(scroll)
|
|
|
|
splitter.setSizes([250, 550])
|
|
|
|
layout.addWidget(splitter)
|
|
|
|
# === Bottom Buttons ===
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
|
|
self.save_btn = QPushButton("💾 Save Loadout")
|
|
self.save_btn.clicked.connect(self._save_loadout)
|
|
self.save_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2E7D32;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #388E3C;
|
|
}
|
|
""")
|
|
button_layout.addWidget(self.save_btn)
|
|
|
|
self.cancel_btn = QPushButton("Cancel")
|
|
self.cancel_btn.clicked.connect(self.reject)
|
|
button_layout.addWidget(self.cancel_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
def _select_weapon(self):
|
|
"""Open weapon selector dialog."""
|
|
from ui.weapon_selector import WeaponSelectorDialog
|
|
|
|
dialog = WeaponSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
weapon = dialog.get_selected_weapon()
|
|
if weapon:
|
|
self._set_weapon(weapon)
|
|
|
|
def _set_weapon(self, weapon: NexusWeapon):
|
|
"""Set weapon and calculate cost."""
|
|
# Calculate cost per shot
|
|
decay_pec = Decimal(str(weapon.decay))
|
|
ammo = Decimal(str(weapon.ammo_burn))
|
|
cost_per_shot = (decay_pec / Decimal("100")) + (ammo * Decimal("0.0001"))
|
|
|
|
# Update UI
|
|
self.weapon_btn.setText(weapon.name[:30])
|
|
self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}")
|
|
self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_weapon = {
|
|
'name': weapon.name,
|
|
'api_id': weapon.id,
|
|
'damage': weapon.damage,
|
|
'decay_pec': decay_pec,
|
|
'ammo_pec': ammo,
|
|
'cost_per_shot': cost_per_shot,
|
|
}
|
|
|
|
self._update_weapon_total()
|
|
self._update_summary()
|
|
|
|
def _select_amplifier(self):
|
|
"""Open weapon amplifier selector."""
|
|
from ui.amplifier_selector import AmplifierSelectorDialog
|
|
|
|
dialog = AmplifierSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
amp = dialog.get_selected_amplifier()
|
|
if amp:
|
|
self._set_amplifier(amp)
|
|
|
|
def _set_amplifier(self, amp_data: dict):
|
|
"""Set weapon amplifier."""
|
|
name = amp_data.get('name', 'Unknown')
|
|
decay_pec = Decimal(str(amp_data.get('decay_pec', 0)))
|
|
damage_bonus = Decimal(str(amp_data.get('damage_bonus', 0)))
|
|
cost_per_shot = decay_pec / Decimal("100")
|
|
|
|
# Update UI
|
|
self.amp_btn.setText(name[:30])
|
|
self.amp_info.setText(f"+{damage_bonus} Damage")
|
|
self.amp_cost_label.setText(f"{cost_per_shot:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_amplifier = {
|
|
'name': name,
|
|
'api_id': amp_data.get('api_id'),
|
|
'decay_pec': decay_pec,
|
|
'damage_bonus': damage_bonus,
|
|
'cost_per_shot': cost_per_shot,
|
|
}
|
|
|
|
self._update_weapon_total()
|
|
self._update_summary()
|
|
|
|
def _update_weapon_total(self):
|
|
"""Update total weapon cost display."""
|
|
weapon = getattr(self, '_pending_weapon', {})
|
|
amp = getattr(self, '_pending_amplifier', {})
|
|
|
|
weapon_cost = weapon.get('cost_per_shot', Decimal("0"))
|
|
amp_cost = amp.get('cost_per_shot', Decimal("0"))
|
|
total = weapon_cost + amp_cost
|
|
|
|
self.total_weapon_cost_label.setText(f"{total:.4f} PED")
|
|
|
|
def _select_armor(self):
|
|
"""Open simplified armor selector."""
|
|
from ui.armor_selector import ArmorSelectorDialog
|
|
|
|
dialog = ArmorSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
result = dialog.get_selected_armor()
|
|
if result:
|
|
self._set_armor(result)
|
|
|
|
def _set_armor(self, armor_data: dict):
|
|
"""Set armor and calculate cost."""
|
|
name = armor_data.get('name', 'Unknown')
|
|
decay_pec = Decimal(str(armor_data.get('decay_pec', 0)))
|
|
cost_per_hit = decay_pec / Decimal("100")
|
|
|
|
# Update UI
|
|
self.armor_btn.setText(name[:30])
|
|
prot_summary = armor_data.get('protection_summary', '')
|
|
self.armor_info.setText(prot_summary[:50] if prot_summary else "No protection data")
|
|
self.armor_cost_label.setText(f"{cost_per_hit:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_armor = {
|
|
'name': name,
|
|
'api_id': armor_data.get('api_id'),
|
|
'decay_pec': decay_pec,
|
|
'cost_per_hit': cost_per_hit,
|
|
}
|
|
|
|
self._update_armor_total()
|
|
self._update_summary()
|
|
|
|
def _select_plating(self):
|
|
"""Open armor plating selector."""
|
|
from ui.plate_selector import PlateSelectorDialog
|
|
|
|
dialog = PlateSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
plate = dialog.selected_plate
|
|
if plate:
|
|
self._set_plating(plate)
|
|
|
|
def _set_plating(self, plate_data: dict):
|
|
"""Set armor plating."""
|
|
name = plate_data.get('name', 'Unknown')
|
|
decay_pec = Decimal(str(plate_data.get('decay_pec', 0)))
|
|
cost_per_hit = decay_pec / Decimal("100")
|
|
|
|
# Update UI
|
|
self.plating_btn.setText(name[:30])
|
|
prot_summary = plate_data.get('protection_summary', '')
|
|
self.plating_info.setText(prot_summary[:40] if prot_summary else "No data")
|
|
self.plating_cost_label.setText(f"{cost_per_hit:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_plating = {
|
|
'name': name,
|
|
'api_id': plate_data.get('api_id'),
|
|
'decay_pec': decay_pec,
|
|
'protection_summary': prot_summary,
|
|
'cost_per_hit': cost_per_hit,
|
|
}
|
|
|
|
self._update_armor_total()
|
|
self._update_summary()
|
|
|
|
def _update_armor_total(self):
|
|
"""Update total armor cost display."""
|
|
armor = getattr(self, '_pending_armor', {})
|
|
plating = getattr(self, '_pending_plating', {})
|
|
|
|
armor_cost = armor.get('cost_per_hit', Decimal("0"))
|
|
plating_cost = plating.get('cost_per_hit', Decimal("0"))
|
|
total = armor_cost + plating_cost
|
|
|
|
self.total_armor_cost_label.setText(f"{total:.4f} PED")
|
|
|
|
def _select_healing(self):
|
|
"""Open healing selector."""
|
|
from ui.healing_selector import HealingSelectorDialog
|
|
|
|
dialog = HealingSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
healing = dialog.get_selected_healing()
|
|
if healing:
|
|
self._set_healing(healing)
|
|
|
|
def _set_healing(self, healing_data: dict):
|
|
"""Set healing and calculate cost."""
|
|
name = healing_data.get('name', 'Unknown')
|
|
decay_pec = Decimal(str(healing_data.get('decay_pec', 0)))
|
|
heal_amount = Decimal(str(healing_data.get('heal_amount', 0)))
|
|
cost_per_heal = decay_pec / Decimal("100")
|
|
|
|
# Update UI
|
|
self.healing_btn.setText(name[:30])
|
|
self.healing_info.setText(f"Heal: {heal_amount} HP")
|
|
self.healing_cost_label.setText(f"{cost_per_heal:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_healing = {
|
|
'name': name,
|
|
'api_id': healing_data.get('api_id'),
|
|
'decay_pec': decay_pec,
|
|
'heal_amount': heal_amount,
|
|
'cost_per_heal': cost_per_heal,
|
|
}
|
|
|
|
self._update_healing_total()
|
|
self._update_summary()
|
|
|
|
def _select_mindforce(self):
|
|
"""Open mindforce implant selector."""
|
|
from ui.mindforce_selector import MindforceSelectorDialog
|
|
|
|
dialog = MindforceSelectorDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
chip = dialog.get_selected_chip()
|
|
if chip:
|
|
self._set_mindforce(chip)
|
|
|
|
def _set_mindforce(self, chip_data: dict):
|
|
"""Set mindforce implant."""
|
|
name = chip_data.get('name', 'Unknown')
|
|
decay_pec = Decimal(str(chip_data.get('decay_pec', 0)))
|
|
heal_amount = Decimal(str(chip_data.get('heal_amount', 0)))
|
|
cost_per_heal = decay_pec / Decimal("100")
|
|
|
|
# Update UI
|
|
self.mindforce_btn.setText(name[:30])
|
|
self.mindforce_info.setText(f"Heal: {heal_amount} HP")
|
|
self.mindforce_cost_label.setText(f"{cost_per_heal:.4f} PED")
|
|
|
|
# Store for saving
|
|
self._pending_mindforce = {
|
|
'name': name,
|
|
'api_id': chip_data.get('api_id'),
|
|
'decay_pec': decay_pec,
|
|
'heal_amount': heal_amount,
|
|
'cost_per_heal': cost_per_heal,
|
|
}
|
|
|
|
self._update_healing_total()
|
|
self._update_summary()
|
|
|
|
def _update_healing_total(self):
|
|
"""Update total healing cost display."""
|
|
healing = getattr(self, '_pending_healing', {})
|
|
mindforce = getattr(self, '_pending_mindforce', {})
|
|
|
|
healing_cost = healing.get('cost_per_heal', Decimal("0"))
|
|
mindforce_cost = mindforce.get('cost_per_heal', Decimal("0"))
|
|
total = healing_cost + mindforce_cost
|
|
|
|
self.total_healing_cost_label.setText(f"{total:.4f} PED")
|
|
|
|
def _update_summary(self):
|
|
"""Update cost summary display."""
|
|
weapon = getattr(self, '_pending_weapon', {})
|
|
amp = getattr(self, '_pending_amplifier', {})
|
|
armor = getattr(self, '_pending_armor', {})
|
|
plating = getattr(self, '_pending_plating', {})
|
|
healing = getattr(self, '_pending_healing', {})
|
|
mindforce = getattr(self, '_pending_mindforce', {})
|
|
|
|
shot = weapon.get('cost_per_shot', Decimal("0")) + amp.get('cost_per_shot', Decimal("0"))
|
|
hit = armor.get('cost_per_hit', Decimal("0")) + plating.get('cost_per_hit', Decimal("0"))
|
|
heal = healing.get('cost_per_heal', Decimal("0")) + mindforce.get('cost_per_heal', Decimal("0"))
|
|
|
|
self.summary_shot.setText(f"{shot:.4f} PED")
|
|
self.summary_hit.setText(f"{hit:.4f} PED")
|
|
self.summary_heal.setText(f"{heal:.4f} PED")
|
|
|
|
def _save_loadout(self):
|
|
"""Save current configuration."""
|
|
name = self.name_edit.text().strip()
|
|
if not name:
|
|
QMessageBox.warning(self, "Missing Name", "Please enter a loadout name")
|
|
return
|
|
|
|
# Build config from pending data
|
|
weapon = getattr(self, '_pending_weapon', {})
|
|
amp = getattr(self, '_pending_amplifier', {})
|
|
armor = getattr(self, '_pending_armor', {})
|
|
plating = getattr(self, '_pending_plating', {})
|
|
healing = getattr(self, '_pending_healing', {})
|
|
mindforce = getattr(self, '_pending_mindforce', {})
|
|
|
|
config = LoadoutConfig(
|
|
name=name,
|
|
# Weapon
|
|
weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")),
|
|
weapon_name=weapon.get('name', 'None'),
|
|
weapon_damage=weapon.get('damage', Decimal("0")),
|
|
weapon_decay_pec=weapon.get('decay_pec', Decimal("0")),
|
|
weapon_ammo_pec=weapon.get('ammo_pec', Decimal("0")),
|
|
weapon_api_id=weapon.get('api_id'),
|
|
# Weapon Amplifier
|
|
weapon_amp_id=amp.get('api_id'),
|
|
weapon_amp_name=amp.get('name', 'None'),
|
|
weapon_amp_decay=amp.get('decay_pec', Decimal("0")),
|
|
weapon_amp_damage_bonus=amp.get('damage_bonus', Decimal("0")),
|
|
# Armor
|
|
armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")),
|
|
armor_name=armor.get('name', 'None'),
|
|
armor_decay_pec=armor.get('decay_pec', Decimal("0")),
|
|
armor_api_id=armor.get('api_id'),
|
|
# Plating
|
|
plating_id=plating.get('api_id'),
|
|
plating_name=plating.get('name', 'None'),
|
|
plating_decay=plating.get('decay_pec', Decimal("0")),
|
|
plating_protection_summary=plating.get('protection_summary', ''),
|
|
# Healing
|
|
healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")),
|
|
healing_name=healing.get('name', 'None'),
|
|
healing_decay_pec=healing.get('decay_pec', Decimal("0")),
|
|
healing_api_id=healing.get('api_id'),
|
|
# Mindforce
|
|
mindforce_implant_id=mindforce.get('api_id'),
|
|
mindforce_implant_name=mindforce.get('name', 'None'),
|
|
mindforce_implant_decay=mindforce.get('decay_pec', Decimal("0")),
|
|
mindforce_implant_heal_amount=mindforce.get('heal_amount', Decimal("0")),
|
|
)
|
|
|
|
# Save to file
|
|
safe_name = "".join(c for c in name if c.isalnum() or c in "._- ").strip()
|
|
if not safe_name:
|
|
safe_name = "unnamed"
|
|
|
|
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_config = config
|
|
self.loadout_saved.emit(config)
|
|
self._load_saved_loadouts()
|
|
|
|
QMessageBox.information(self, "Saved", f"Loadout '{name}' saved!")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save: {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))
|
|
|
|
# Tooltip with costs
|
|
tooltip = (
|
|
f"Weapon: {config.weapon_name}\n"
|
|
f" + Amp: {config.weapon_amp_name if config.weapon_amp_id else 'None'}\n"
|
|
f"Armor: {config.armor_name}\n"
|
|
f" + Plating: {config.plating_name if config.plating_id else 'None'}\n"
|
|
f"Healing: {config.healing_name}\n"
|
|
f" + Chip: {config.mindforce_implant_name if config.mindforce_implant_id else 'None'}\n"
|
|
f"Cost/Shot: {config.get_total_weapon_cost_per_shot():.4f} PED\n"
|
|
f"Cost/Hit: {config.get_total_armor_cost_per_hit():.4f} PED\n"
|
|
f"Cost/Heal: {config.get_total_healing_cost_per_heal():.4f} PED"
|
|
)
|
|
item.setToolTip(tooltip)
|
|
self.saved_list.addItem(item)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load {filepath}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to list loadouts: {e}")
|
|
|
|
def _on_loadout_selected(self, item: QListWidgetItem):
|
|
"""Load selected loadout into UI."""
|
|
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._load_config_into_ui(config)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to load: {e}")
|
|
|
|
def _on_loadout_double_clicked(self, item: QListWidgetItem):
|
|
"""Double-click to select and close."""
|
|
self._on_loadout_selected(item)
|
|
self.accept()
|
|
|
|
def _load_config_into_ui(self, config: LoadoutConfig):
|
|
"""Load config values into UI fields."""
|
|
self.name_edit.setText(config.name)
|
|
|
|
# Weapon
|
|
if config.weapon_name != "None":
|
|
self.weapon_btn.setText(config.weapon_name[:30])
|
|
self.weapon_info.setText(f"Damage: {config.weapon_damage}")
|
|
self.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED")
|
|
self._pending_weapon = {
|
|
'name': config.weapon_name,
|
|
'api_id': config.weapon_api_id,
|
|
'damage': config.weapon_damage,
|
|
'decay_pec': config.weapon_decay_pec,
|
|
'ammo_pec': config.weapon_ammo_pec,
|
|
'cost_per_shot': config.weapon_cost_per_shot,
|
|
}
|
|
|
|
# Weapon Amplifier
|
|
if config.weapon_amp_id:
|
|
self.amp_btn.setText(config.weapon_amp_name[:30])
|
|
self.amp_info.setText(f"+{config.weapon_amp_damage_bonus} Damage")
|
|
amp_cost = config.weapon_amp_decay / Decimal("100")
|
|
self.amp_cost_label.setText(f"{amp_cost:.4f} PED")
|
|
self._pending_amplifier = {
|
|
'name': config.weapon_amp_name,
|
|
'api_id': config.weapon_amp_id,
|
|
'decay_pec': config.weapon_amp_decay,
|
|
'damage_bonus': config.weapon_amp_damage_bonus,
|
|
'cost_per_shot': amp_cost,
|
|
}
|
|
|
|
# Armor
|
|
if config.armor_name != "None":
|
|
self.armor_btn.setText(config.armor_name[:30])
|
|
self.armor_info.setText("Loaded from save")
|
|
self.armor_cost_label.setText(f"{config.armor_cost_per_hit:.4f} PED")
|
|
self._pending_armor = {
|
|
'name': config.armor_name,
|
|
'api_id': config.armor_api_id,
|
|
'decay_pec': config.armor_decay_pec,
|
|
'cost_per_hit': config.armor_cost_per_hit,
|
|
}
|
|
|
|
# Plating
|
|
if config.plating_id:
|
|
self.plating_btn.setText(config.plating_name[:30])
|
|
self.plating_info.setText(config.plating_protection_summary[:40] if config.plating_protection_summary else "No data")
|
|
plating_cost = config.plating_decay / Decimal("100")
|
|
self.plating_cost_label.setText(f"{plating_cost:.4f} PED")
|
|
self._pending_plating = {
|
|
'name': config.plating_name,
|
|
'api_id': config.plating_id,
|
|
'decay_pec': config.plating_decay,
|
|
'protection_summary': config.plating_protection_summary,
|
|
'cost_per_hit': plating_cost,
|
|
}
|
|
|
|
# Healing
|
|
if config.healing_name != "None":
|
|
self.healing_btn.setText(config.healing_name[:30])
|
|
self.healing_cost_label.setText(f"{config.healing_cost_per_heal:.4f} PED")
|
|
self._pending_healing = {
|
|
'name': config.healing_name,
|
|
'api_id': config.healing_api_id,
|
|
'decay_pec': config.healing_decay_pec,
|
|
'cost_per_heal': config.healing_cost_per_heal,
|
|
}
|
|
|
|
# Mindforce
|
|
if config.mindforce_implant_id:
|
|
self.mindforce_btn.setText(config.mindforce_implant_name[:30])
|
|
mindforce_cost = config.mindforce_implant_decay / Decimal("100")
|
|
self.mindforce_cost_label.setText(f"{mindforce_cost:.4f} PED")
|
|
self._pending_mindforce = {
|
|
'name': config.mindforce_implant_name,
|
|
'api_id': config.mindforce_implant_id,
|
|
'decay_pec': config.mindforce_implant_decay,
|
|
'heal_amount': config.mindforce_implant_heal_amount,
|
|
'cost_per_heal': mindforce_cost,
|
|
}
|
|
|
|
self._update_weapon_total()
|
|
self._update_armor_total()
|
|
self._update_healing_total()
|
|
self._update_summary()
|
|
self.current_config = config
|
|
|
|
def _new_loadout(self):
|
|
"""Clear all fields for new loadout."""
|
|
self.name_edit.clear()
|
|
|
|
# Reset weapon section
|
|
self.weapon_btn.setText("Select Weapon...")
|
|
self.weapon_info.setText("None selected")
|
|
self.weapon_cost_label.setText("0.0000 PED")
|
|
self.amp_btn.setText("Select Amplifier...")
|
|
self.amp_info.setText("None selected")
|
|
self.amp_cost_label.setText("0.0000 PED")
|
|
self.total_weapon_cost_label.setText("0.0000 PED")
|
|
|
|
# Reset armor section
|
|
self.armor_btn.setText("Select Armor...")
|
|
self.armor_info.setText("None selected")
|
|
self.armor_cost_label.setText("0.0000 PED")
|
|
self.plating_btn.setText("Select Plating...")
|
|
self.plating_info.setText("None selected")
|
|
self.plating_cost_label.setText("0.0000 PED")
|
|
self.total_armor_cost_label.setText("0.0000 PED")
|
|
|
|
# Reset healing section
|
|
self.healing_btn.setText("Select Healing...")
|
|
self.healing_info.setText("None selected")
|
|
self.healing_cost_label.setText("0.0000 PED")
|
|
self.mindforce_btn.setText("Select Mindforce Chip...")
|
|
self.mindforce_info.setText("None selected")
|
|
self.mindforce_cost_label.setText("0.0000 PED")
|
|
self.total_healing_cost_label.setText("0.0000 PED")
|
|
|
|
# Clear pending data
|
|
self._pending_weapon = None
|
|
self._pending_amplifier = None
|
|
self._pending_armor = None
|
|
self._pending_plating = None
|
|
self._pending_healing = None
|
|
self._pending_mindforce = None
|
|
|
|
self._update_summary()
|
|
self.current_config = None
|
|
|
|
def _delete_loadout(self):
|
|
"""Delete 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"Delete '{name}'?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
try:
|
|
os.remove(filepath)
|
|
self._load_saved_loadouts()
|
|
self._new_loadout()
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to delete: {e}")
|
|
|
|
def get_config(self) -> Optional[LoadoutConfig]:
|
|
"""Get current configuration."""
|
|
return self.current_config
|
|
|
|
|
|
# ============================================================================
|
|
# Backward Compatibility
|
|
# ============================================================================
|
|
|
|
# Keep old names for imports
|
|
LoadoutManager = LoadoutManagerDialog
|