refactor: simplified cost-focused Loadout Manager

- New LoadoutManagerSimple with clean cost-focused design
- LoadoutConfig now stores only: cost_per_shot/hit/heal + display names
- Legacy format support for backward compatibility
- Simplified LoadoutSelectionDialog with clear cost preview
- Updated MainWindow to use new simplified structure
- Removed 3 overlapping armor systems, replaced with single decay value
- JSON serialization is now simple and reliable

Key principle: Only store what's needed for cost tracking.
This commit is contained in:
LemonNexus 2026-02-09 21:41:55 +00:00
parent 83084252cc
commit cdc9f5b825
3 changed files with 989 additions and 56 deletions

View File

@ -0,0 +1,667 @@
"""
Lemontropia Suite - Loadout Manager UI v4.0
Simplified cost-focused loadout system.
"""
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,
)
from PyQt6.QtCore import Qt, pyqtSignal
from core.nexus_full_api import get_nexus_api, NexusWeapon
logger = logging.getLogger(__name__)
# ============================================================================
# Simple Cost-Focused Loadout Config
# ============================================================================
@dataclass
class LoadoutConfig:
"""Simple loadout configuration focused on cost tracking.
Core principle: Only store what's needed for cost calculations.
Everything else is display metadata.
"""
# Identity
name: str = "Unnamed"
version: int = 2 # Version 2 = simplified format
# === COST DATA (Required for tracking) ===
# All values in PED (not PEC)
weapon_cost_per_shot: Decimal = Decimal("0")
armor_cost_per_hit: Decimal = Decimal("0")
healing_cost_per_heal: Decimal = Decimal("0")
# === DISPLAY METADATA (For UI only) ===
weapon_name: str = "None"
weapon_damage: Decimal = Decimal("0")
weapon_decay_pec: Decimal = Decimal("0") # Raw for reference
weapon_ammo_pec: Decimal = Decimal("0") # Raw for reference
armor_name: str = "None"
armor_decay_pec: Decimal = Decimal("0") # Raw for reference
healing_name: str = "None"
healing_decay_pec: Decimal = Decimal("0") # Raw for reference
# === API REFERENCES (For re-loading from API) ===
weapon_api_id: Optional[int] = None
armor_api_id: Optional[int] = None
healing_api_id: Optional[int] = None
def to_dict(self) -> dict:
"""Serialize to simple dictionary."""
return {
'name': self.name,
'version': self.version,
'weapon_cost_per_shot': str(self.weapon_cost_per_shot),
'armor_cost_per_hit': str(self.armor_cost_per_hit),
'healing_cost_per_heal': str(self.healing_cost_per_heal),
'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),
'armor_name': self.armor_name,
'armor_decay_pec': str(self.armor_decay_pec),
'healing_name': self.healing_name,
'healing_decay_pec': str(self.healing_decay_pec),
'weapon_api_id': self.weapon_api_id,
'armor_api_id': self.armor_api_id,
'healing_api_id': self.healing_api_id,
}
@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(data)
else:
return cls._from_v2(data)
@classmethod
def _from_v2(cls, data: dict) -> "LoadoutConfig":
"""Parse version 2 (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=2,
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
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'),
armor_name=data.get('armor_name', 'None'),
armor_decay_pec=get_decimal('armor_decay_pec'),
healing_name=data.get('healing_name', 'None'),
healing_decay_pec=get_decimal('healing_decay_pec'),
weapon_api_id=data.get('weapon_api_id'),
armor_api_id=data.get('armor_api_id'),
healing_api_id=data.get('healing_api_id'),
)
@classmethod
def _from_legacy(cls, data: dict) -> "LoadoutConfig":
"""Convert legacy format to new simple 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=2,
weapon_cost_per_shot=weapon_cost_per_shot,
armor_cost_per_hit=armor_cost_per_hit,
healing_cost_per_heal=healing_cost_per_heal,
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_name=data.get('armor_set_name', data.get('armor_name', 'None')),
armor_decay_pec=armor_decay,
healing_name=data.get('heal_name', 'None'),
healing_decay_pec=heal_decay,
)
def get_summary(self) -> Dict[str, Any]:
"""Get cost summary for display."""
return {
'name': self.name,
'weapon': self.weapon_name,
'armor': self.armor_name,
'healing': self.healing_name,
'cost_per_shot': self.weapon_cost_per_shot,
'cost_per_hit': self.armor_cost_per_hit,
'cost_per_heal': self.healing_cost_per_heal,
}
# ============================================================================
# Simple Loadout Manager Dialog
# ============================================================================
class LoadoutManagerDialog(QDialog):
"""Simplified loadout manager focused on cost configuration."""
loadout_saved = pyqtSignal(LoadoutConfig)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Loadout Manager")
self.setMinimumSize(600, 500)
# State
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
self.config_dir.mkdir(parents=True, exist_ok=True)
self.current_config: Optional[LoadoutConfig] = None
# Cached API data
self._cached_weapons: Optional[list] = None
self._cached_armors: Optional[list] = None
self._cached_healing: Optional[list] = None
self._setup_ui()
self._load_saved_loadouts()
def _setup_ui(self):
"""Setup simplified 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")
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
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.setContentsMargins(0, 0, 0, 0)
# -- Weapon Section --
weapon_group = QGroupBox("⚔️ Weapon")
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_decay_label = QLabel("0 PEC")
weapon_layout.addRow("Decay:", self.weapon_decay_label)
self.weapon_ammo_label = QLabel("0")
weapon_layout.addRow("Ammo:", self.weapon_ammo_label)
self.weapon_cost_label = QLabel("0.0000 PED")
self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
weapon_layout.addRow("Cost/Shot:", self.weapon_cost_label)
right_layout.addWidget(weapon_group)
# -- Armor Section --
armor_group = QGroupBox("🛡️ Armor")
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("Cost/Hit:", self.armor_cost_label)
right_layout.addWidget(armor_group)
# -- Healing Section --
healing_group = QGroupBox("💚 Healing")
healing_layout = QFormLayout(healing_group)
self.healing_btn = QPushButton("Select Healing...")
self.healing_btn.clicked.connect(self._select_healing)
healing_layout.addRow("Healing:", 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("Cost/Heal:", self.healing_cost_label)
right_layout.addWidget(healing_group)
# -- Summary Section --
summary_group = QGroupBox("💰 Session Cost Summary")
summary_layout = QGridLayout(summary_group)
summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0)
self.summary_shot = QLabel("0.0000 PED")
summary_layout.addWidget(self.summary_shot, 0, 1)
summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0)
self.summary_hit = QLabel("0.0000 PED")
summary_layout.addWidget(self.summary_hit, 1, 1)
summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0)
self.summary_heal = QLabel("0.0000 PED")
summary_layout.addWidget(self.summary_heal, 2, 1)
summary_layout.setColumnStretch(1, 1)
right_layout.addWidget(summary_group)
right_layout.addStretch()
splitter.addWidget(right_widget)
splitter.setSizes([200, 400])
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))
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}")
self.weapon_decay_label.setText(f"{decay_pec} PEC")
self.weapon_ammo_label.setText(f"{ammo}")
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_summary()
def _select_armor(self):
"""Open simplified armor selector."""
from ui.armor_selection_dialog import ArmorSelectionDialog
dialog = ArmorSelectionDialog(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."""
# armor_data has: name, decay_pec, protection_summary
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_summary()
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_summary()
def _update_summary(self):
"""Update cost summary display."""
shot = getattr(self, '_pending_weapon', {}).get('cost_per_shot', Decimal("0"))
hit = getattr(self, '_pending_armor', {}).get('cost_per_hit', Decimal("0"))
heal = getattr(self, '_pending_healing', {}).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', {})
armor = getattr(self, '_pending_armor', {})
healing = getattr(self, '_pending_healing', {})
config = LoadoutConfig(
name=name,
weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")),
armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")),
healing_cost_per_heal=healing.get('cost_per_heal', 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")),
armor_name=armor.get('name', 'None'),
armor_decay_pec=armor.get('decay_pec', Decimal("0")),
healing_name=healing.get('name', 'None'),
healing_decay_pec=healing.get('decay_pec', Decimal("0")),
weapon_api_id=weapon.get('api_id'),
armor_api_id=armor.get('api_id'),
healing_api_id=healing.get('api_id'),
)
# 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"Armor: {config.armor_name}\n"
f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n"
f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n"
f"Cost/Heal: {config.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_decay_label.setText(f"{config.weapon_decay_pec} PEC")
self.weapon_ammo_label.setText(f"{config.weapon_ammo_pec}")
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,
}
# 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,
}
# 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,
}
self._update_summary()
self.current_config = config
def _new_loadout(self):
"""Clear all fields for new loadout."""
self.name_edit.clear()
self.weapon_btn.setText("Select Weapon...")
self.weapon_info.setText("None selected")
self.weapon_decay_label.setText("0 PEC")
self.weapon_ammo_label.setText("0")
self.weapon_cost_label.setText("0.0000 PED")
self.armor_btn.setText("Select Armor...")
self.armor_info.setText("None selected")
self.armor_cost_label.setText("0.0000 PED")
self.healing_btn.setText("Select Healing...")
self.healing_info.setText("None selected")
self.healing_cost_label.setText("0.0000 PED")
self._pending_weapon = None
self._pending_armor = None
self._pending_healing = 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

View File

@ -0,0 +1,262 @@
"""
Lemontropia Suite - Loadout Selection Dialog v2.0
Simplified cost-focused selection.
"""
import json
import logging
from decimal import Decimal
from pathlib import Path
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QListWidget, QListWidgetItem,
QMessageBox, QWidget, QGridLayout
)
from PyQt6.QtCore import Qt, pyqtSignal
from ui.loadout_manager_simple import LoadoutConfig
logger = logging.getLogger(__name__)
class LoadoutSelectionDialog(QDialog):
"""Simplified dialog for selecting a loadout to start a session.
Emits cost data that can be used directly by MainWindow for tracking.
"""
# Signal emits: {
# 'id': 0, # 0 = file-based
# 'name': 'Loadout Name',
# 'source': 'file',
# 'costs': {
# 'cost_per_shot': Decimal,
# 'cost_per_hit': Decimal,
# 'cost_per_heal': Decimal,
# },
# 'display': {
# 'weapon_name': str,
# 'armor_name': str,
# 'healing_name': str,
# }
# }
loadout_selected = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Loadout for Session")
self.setMinimumSize(500, 400)
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
self.selected_loadout = None
self._setup_ui()
self._load_loadouts()
def _setup_ui(self):
"""Setup simple UI."""
layout = QVBoxLayout(self)
layout.setSpacing(10)
# Header
header = QLabel("Select a loadout to track costs during your session:")
header.setStyleSheet("font-size: 12px; color: #888;")
layout.addWidget(header)
# Loadout list
self.loadout_list = QListWidget()
self.loadout_list.itemClicked.connect(self._on_select)
self.loadout_list.itemDoubleClicked.connect(self._on_double_click)
layout.addWidget(self.loadout_list)
# Preview panel
self.preview = QWidget()
preview_layout = QGridLayout(self.preview)
# Weapon
preview_layout.addWidget(QLabel("⚔️ Weapon:"), 0, 0)
self.preview_weapon = QLabel("-")
preview_layout.addWidget(self.preview_weapon, 0, 1)
preview_layout.addWidget(QLabel(" Cost/Shot:"), 1, 0)
self.preview_cost_shot = QLabel("-")
self.preview_cost_shot.setStyleSheet("color: #7FFF7F;")
preview_layout.addWidget(self.preview_cost_shot, 1, 1)
# Armor
preview_layout.addWidget(QLabel("🛡️ Armor:"), 2, 0)
self.preview_armor = QLabel("-")
preview_layout.addWidget(self.preview_armor, 2, 1)
preview_layout.addWidget(QLabel(" Cost/Hit:"), 3, 0)
self.preview_cost_hit = QLabel("-")
self.preview_cost_hit.setStyleSheet("color: #7FFF7F;")
preview_layout.addWidget(self.preview_cost_hit, 3, 1)
# Healing
preview_layout.addWidget(QLabel("💚 Healing:"), 4, 0)
self.preview_healing = QLabel("-")
preview_layout.addWidget(self.preview_healing, 4, 1)
preview_layout.addWidget(QLabel(" Cost/Heal:"), 5, 0)
self.preview_cost_heal = QLabel("-")
self.preview_cost_heal.setStyleSheet("color: #7FFF7F;")
preview_layout.addWidget(self.preview_cost_heal, 5, 1)
preview_layout.setColumnStretch(1, 1)
layout.addWidget(self.preview)
# Buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
self.skip_btn = QPushButton("Skip (No Cost Tracking)")
self.skip_btn.clicked.connect(self._on_skip)
self.skip_btn.setStyleSheet("color: #888;")
button_layout.addWidget(self.skip_btn)
self.ok_btn = QPushButton("Start Session ▶")
self.ok_btn.clicked.connect(self._on_accept)
self.ok_btn.setEnabled(False)
self.ok_btn.setStyleSheet("""
QPushButton {
background-color: #2E7D32;
color: white;
padding: 8px 16px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #333;
color: #666;
}
""")
button_layout.addWidget(self.ok_btn)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def _load_loadouts(self):
"""Load all saved loadouts."""
self.loadout_list.clear()
self.loadouts = []
if not self.config_dir.exists():
return
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)
# Build display text
has_costs = (
config.weapon_cost_per_shot > 0 or
config.armor_cost_per_hit > 0 or
config.healing_cost_per_heal > 0
)
display = f"📋 {config.name}"
if has_costs:
display += f" (💰 {config.weapon_cost_per_shot:.3f}/shot)"
item = QListWidgetItem(display)
item.setData(Qt.ItemDataRole.UserRole, config)
# Tooltip
tooltip = (
f"Weapon: {config.weapon_name}\n"
f"Armor: {config.armor_name}\n"
f"Healing: {config.healing_name}\n"
f"\n"
f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n"
f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n"
f"Cost/Heal: {config.healing_cost_per_heal:.4f} PED"
)
item.setToolTip(tooltip)
self.loadout_list.addItem(item)
self.loadouts.append(config)
except Exception as e:
logger.error(f"Failed to load {filepath}: {e}")
except Exception as e:
logger.error(f"Failed to list loadouts: {e}")
if not self.loadouts:
item = QListWidgetItem("(No saved loadouts)")
item.setFlags(Qt.ItemFlag.NoItemFlags)
self.loadout_list.addItem(item)
def _on_select(self, item: QListWidgetItem):
"""Update preview when loadout selected."""
config = item.data(Qt.ItemDataRole.UserRole)
if not isinstance(config, LoadoutConfig):
return
self.selected_loadout = config
# Update preview
self.preview_weapon.setText(config.weapon_name if config.weapon_name != "None" else "Not set")
self.preview_armor.setText(config.armor_name if config.armor_name != "None" else "Not set")
self.preview_healing.setText(config.healing_name if config.healing_name != "None" else "Not set")
self.preview_cost_shot.setText(f"{config.weapon_cost_per_shot:.4f} PED")
self.preview_cost_hit.setText(f"{config.armor_cost_per_hit:.4f} PED")
self.preview_cost_heal.setText(f"{config.healing_cost_per_heal:.4f} PED")
self.ok_btn.setEnabled(True)
def _on_double_click(self, item: QListWidgetItem):
"""Double-click to select immediately."""
self._on_select(item)
self._on_accept()
def _on_skip(self):
"""Start session without cost tracking."""
self.loadout_selected.emit({
'id': 0,
'name': 'No Loadout',
'source': 'none',
'costs': {
'cost_per_shot': Decimal('0'),
'cost_per_hit': Decimal('0'),
'cost_per_heal': Decimal('0'),
},
'display': {
'weapon_name': 'None',
'armor_name': 'None',
'healing_name': 'None',
}
})
self.accept()
def _on_accept(self):
"""Emit selected loadout and close."""
if not self.selected_loadout:
QMessageBox.warning(self, "No Selection", "Please select a loadout")
return
config = self.selected_loadout
self.loadout_selected.emit({
'id': 0, # File-based
'name': config.name,
'source': 'file',
'costs': {
'cost_per_shot': config.weapon_cost_per_shot,
'cost_per_hit': config.armor_cost_per_hit,
'cost_per_heal': config.healing_cost_per_heal,
},
'display': {
'weapon_name': config.weapon_name,
'armor_name': config.armor_name,
'healing_name': config.healing_name,
}
})
self.accept()

View File

@ -936,23 +936,23 @@ class MainWindow(QMainWindow):
# Show HUD and start session tracking # Show HUD and start session tracking
self.hud.show() self.hud.show()
# Get gear names from loadout selection if available, otherwise fall back to selected weapon # Get gear names and costs from simplified loadout structure
weapon_name = getattr(self, '_session_weapon_name', None) or self._selected_weapon or "Unknown" session_display = getattr(self, '_session_display', {})
armor_name = getattr(self, '_session_armor_name', None) or "None" session_costs = getattr(self, '_session_costs', {})
healing_name = getattr(self, '_session_healing_name', None) or "None"
weapon_name = session_display.get('weapon_name', self._selected_weapon or "Unknown")
armor_name = session_display.get('armor_name', "None")
healing_name = session_display.get('healing_name', "None")
loadout_name = "Loadout" if session_costs else "Default"
weapon_stats = self._selected_weapon_stats or {} weapon_stats = self._selected_weapon_stats or {}
weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0))) weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0)))
weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0))) weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0)))
# Get loadout name from session selection # Get cost data from simplified structure
loadout_info = getattr(self, '_session_loadout_info', None) cost_per_shot = session_costs.get('cost_per_shot', Decimal('0'))
loadout_name = loadout_info.get('name', 'Default') if loadout_info else "Default" cost_per_hit = session_costs.get('cost_per_hit', Decimal('0'))
cost_per_heal = session_costs.get('cost_per_heal', Decimal('0'))
# Get cost data from session loadout
cost_per_shot = getattr(self, '_session_cost_per_shot', Decimal('0'))
cost_per_hit = getattr(self, '_session_cost_per_hit', Decimal('0'))
cost_per_heal = getattr(self, '_session_cost_per_heal', Decimal('0'))
self.hud.start_session( self.hud.start_session(
weapon=weapon_name, weapon=weapon_name,
@ -966,12 +966,8 @@ class MainWindow(QMainWindow):
cost_per_heal=cost_per_heal cost_per_heal=cost_per_heal
) )
# Set up cost tracker if loadout selected (database-based only) # Simple cost tracking - no database required
if loadout_info and loadout_info.get('id') and loadout_info.get('source') == 'db': self.log_info("CostTracker", "Cost tracking enabled with pre-calculated values")
self._setup_session_cost_tracker(loadout_info)
else:
# For JSON-based loadouts, use manual cost tracking based on extracted values
self.log_info("CostTracker", "Using manual cost tracking for file-based loadout")
self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Armor: {armor_name}, Healing: {healing_name}, Loadout: {loadout_name}") self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Armor: {armor_name}, Healing: {healing_name}, Loadout: {loadout_name}")
@ -1239,45 +1235,41 @@ class MainWindow(QMainWindow):
"""Handle start session button - shows loadout selection first.""" """Handle start session button - shows loadout selection first."""
if self.current_project and self.session_state == SessionState.IDLE: if self.current_project and self.session_state == SessionState.IDLE:
# Show loadout selection dialog # Show loadout selection dialog
from ui.loadout_selection_dialog import LoadoutSelectionDialog from ui.loadout_selection_dialog_simple import LoadoutSelectionDialog
dialog = LoadoutSelectionDialog(self) dialog = LoadoutSelectionDialog(self)
dialog.loadout_selected.connect(self._on_loadout_selected_for_session) dialog.loadout_selected.connect(self._on_loadout_selected_for_session)
dialog.rejected.connect(lambda: self.log_info("Session", "Session start cancelled - no loadout selected")) dialog.rejected.connect(lambda: self.log_info("Session", "Session start cancelled - no loadout selected"))
dialog.exec() dialog.exec()
def _on_loadout_selected_for_session(self, loadout_info: dict): def _on_loadout_selected_for_session(self, loadout_info: dict):
"""Handle loadout selection and start session.""" """Handle loadout selection and start session - simplified cost-focused version."""
loadout_id = loadout_info.get('id', 0)
loadout_name = loadout_info.get('name', 'No Loadout') loadout_name = loadout_info.get('name', 'No Loadout')
loadout_data = loadout_info.get('data', {}) costs = loadout_info.get('costs', {})
display = loadout_info.get('display', {})
if loadout_id > 0: # Store cost data for session tracking
self.log_info("Session", f"Starting session with loadout: {loadout_name} (ID: {loadout_id})")
# Store the selected loadout info for use in start_session
self._session_loadout_info = loadout_info
# Extract gear names from loadout data
self._session_weapon_name = loadout_data.get('weapon_name', 'Unknown')
self._session_armor_name = loadout_data.get('armor_set_name', 'Unknown')
self._session_healing_name = loadout_data.get('heal_name', 'Unknown')
# Extract cost data for session tracking (even for JSON-based loadouts)
from decimal import Decimal from decimal import Decimal
self._session_cost_per_shot = Decimal(str(loadout_data.get('weapon_decay_pec', 0))) / Decimal('100') + \ self._session_costs = {
Decimal(str(loadout_data.get('weapon_ammo_pec', 0))) * Decimal('0.0001') 'cost_per_shot': costs.get('cost_per_shot', Decimal('0')),
self._session_cost_per_hit = Decimal(str(loadout_data.get('armor_decay_pec', 0))) / Decimal('100') 'cost_per_hit': costs.get('cost_per_hit', Decimal('0')),
self._session_cost_per_heal = Decimal(str(loadout_data.get('heal_cost_pec', 0))) / Decimal('100') 'cost_per_heal': costs.get('cost_per_heal', Decimal('0')),
}
self.log_info("SessionCosts", f"Cost/Shot: {self._session_cost_per_shot:.4f}, Cost/Hit: {self._session_cost_per_hit:.4f}, Cost/Heal: {self._session_cost_per_heal:.4f}") # Store display data for HUD
self._session_display = {
'weapon_name': display.get('weapon_name', 'None'),
'armor_name': display.get('armor_name', 'None'),
'healing_name': display.get('healing_name', 'None'),
}
if any(self._session_costs.values()):
self.log_info("Session", f"Starting with loadout: {loadout_name}")
self.log_info("SessionCosts",
f"Shot: {self._session_costs['cost_per_shot']:.4f} PED, "
f"Hit: {self._session_costs['cost_per_hit']:.4f} PED, "
f"Heal: {self._session_costs['cost_per_heal']:.4f} PED")
else: else:
self.log_info("Session", "Starting session without loadout") self.log_info("Session", f"Starting with loadout: {loadout_name} (no costs configured)")
self._session_loadout_info = None
self._session_weapon_name = None
self._session_armor_name = None
self._session_healing_name = None
self._session_cost_per_shot = None
self._session_cost_per_hit = None
self._session_cost_per_heal = None
# Now start the session # Now start the session
if self.current_project: if self.current_project:
@ -1462,25 +1454,37 @@ class MainWindow(QMainWindow):
def on_loadout_manager(self): def on_loadout_manager(self):
"""Open Loadout Manager dialog.""" """Open Loadout Manager dialog."""
from ui.loadout_manager import LoadoutManagerDialog from ui.loadout_manager_simple import LoadoutManagerDialog
dialog = LoadoutManagerDialog(self) dialog = LoadoutManagerDialog(self)
dialog.loadout_saved.connect(self.on_loadout_selected) dialog.loadout_saved.connect(self.on_loadout_selected)
dialog.exec() dialog.exec()
def on_loadout_selected(self, loadout): def on_loadout_selected(self, loadout):
"""Handle loadout selection from Loadout Manager.""" """Handle loadout selection from Loadout Manager - simplified version."""
self._selected_loadout = loadout self._selected_loadout = loadout
self.log_info("Loadout", f"Selected loadout: {loadout.name}") self.log_info("Loadout", f"Selected loadout: {loadout.name}")
# Update selected gear from loadout # Update selected gear from loadout (simplified structure)
if hasattr(loadout, 'weapon_name'): if hasattr(loadout, 'weapon_name'):
self._selected_weapon = loadout.weapon_name self._selected_weapon = loadout.weapon_name
if hasattr(loadout, 'heal_cost_pec'): if hasattr(loadout, 'healing_name'):
# Create medical tool stats from loadout heal cost self._selected_medical_tool = loadout.healing_name
self._selected_medical_tool = loadout.heal_name
self._selected_medical_tool_stats = { self._selected_medical_tool_stats = {
'decay': float(loadout.heal_cost_pec), 'decay': float(loadout.healing_decay_pec),
'cost_per_heal': float(loadout.heal_cost_pec) / 100.0, # Convert PEC to PED 'cost_per_heal': float(loadout.healing_cost_per_heal),
}
# Store simplified costs for session
if hasattr(loadout, 'weapon_cost_per_shot'):
self._session_costs = {
'cost_per_shot': loadout.weapon_cost_per_shot,
'cost_per_hit': loadout.armor_cost_per_hit,
'cost_per_heal': loadout.healing_cost_per_heal,
}
if hasattr(loadout, 'weapon_name'):
self._session_display = {
'weapon_name': loadout.weapon_name,
'armor_name': loadout.armor_name,
'healing_name': loadout.healing_name,
} }
def on_select_gear(self, gear_type: str = "weapon"): def on_select_gear(self, gear_type: str = "weapon"):