feat(armor): implement official decay formula (VU 15.15)

- Formula: Decay = damage * 0.05 * (1 - durability/100000)
- Added armor_decay.py with complete calculation module
- Database of 30+ armors with durability values
- Economy comparison: Ghost (20.41) → Perseus L (23.53) hp/pec
- Example: 15 dmg on Ghost = 0.00735 PED decay
This commit is contained in:
LemonNexus 2026-02-09 10:27:05 +00:00
parent 08aec368a9
commit ae182f408b
7 changed files with 1173 additions and 7 deletions

224
core/armor_decay.py Normal file
View File

@ -0,0 +1,224 @@
"""
Armor Decay Calculator for Lemontropia Suite
Implements the official VU 15.15 armor decay formula.
"""
from decimal import Decimal
from typing import Dict, Optional
from dataclasses import dataclass
# Armor durability database (from official guide)
ARMOR_DURABILITY: Dict[str, int] = {
# Unlimited Armors
"Ghost": 2000,
"Gremlin": 2950,
"Adjusted Nemesis": 3400,
"Angel": 4000,
"Hero": 3500,
"Dragon": 3000,
"Gorgon": 4500,
"Shogun": 2800,
"Viking": 3200,
"Titan": 3800,
"Demon": 4200,
"Shadow": 3600,
"Warrior": 3100,
"Guardian": 3300,
"Sentinel": 3900,
"Pirate": 2700,
"Swamp": 2600,
"Desert": 2900,
"Arctic": 3400,
"Jungle": 2500,
"Mountain": 3700,
"Forest": 2400,
"Urban": 3500,
"Combat": 4100,
"Assault": 4300,
"Recon": 2300,
"Spec Ops": 4400,
# Limited Armors
"Martial (L)": 13000,
"Mayhem (L)": 13300,
"Angel (L)": 14000,
"Perseus (L)": 15000,
"Moonshine (L)": 15400,
"Eon (L)": 12000,
"Hermes (L)": 12500,
"Tiger (L)": 11000,
"Spartacus (L)": 11500,
"Vain (L)": 11800,
}
# Default durability for unknown armors
DEFAULT_DURABILITY = 2000 # Same as Ghost
def calculate_armor_decay(damage_absorbed: Decimal, durability: int) -> Decimal:
"""Calculate armor decay in PED.
Formula: Decay = damage * 0.05 * (1 - durability/100000)
Args:
damage_absorbed: Amount of damage absorbed by armor (in HP)
durability: Armor durability stat
Returns:
Decay cost in PED
"""
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
decay_pec = damage_absorbed * Decimal("0.05") * durability_factor
return decay_pec / Decimal(100) # Convert PEC to PED
def calculate_hp_per_pec(durability: int) -> Decimal:
"""Calculate armor economy in hp/pec.
Args:
durability: Armor durability stat
Returns:
Economy rating in hp/pec (higher is better)
"""
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
return Decimal("20") / durability_factor
def calculate_protection_cost_per_100_ped(durability: int) -> int:
"""Calculate how much damage 100 PED of decay will absorb.
Args:
durability: Armor durability stat
Returns:
Damage absorbed per 100 PED decay
"""
hp_per_pec = calculate_hp_per_pec(durability)
return int(hp_per_pec * 10000) # 100 PED = 10,000 PEC
@dataclass
class ArmorPiece:
"""Represents a single armor piece."""
name: str
slot: str # 'head', 'chest', 'arms', 'hands', 'legs', 'feet'
durability: int
protection_impact: Decimal = Decimal("0")
protection_cut: Decimal = Decimal("0")
protection_stab: Decimal = Decimal("0")
protection_burn: Decimal = Decimal("0")
protection_cold: Decimal = Decimal("0")
protection_acid: Decimal = Decimal("0")
protection_electric: Decimal = Decimal("0")
def calculate_decay(self, damage_absorbed: Decimal) -> Decimal:
"""Calculate decay for this piece."""
return calculate_armor_decay(damage_absorbed, self.durability)
def get_economy(self) -> Decimal:
"""Get hp/pec economy rating."""
return calculate_hp_per_pec(self.durability)
@dataclass
class ArmorSet:
"""Represents a complete armor set (7 pieces)."""
name: str
head: Optional[ArmorPiece] = None
chest: Optional[ArmorPiece] = None
left_arm: Optional[ArmorPiece] = None
right_arm: Optional[ArmorPiece] = None
left_hand: Optional[ArmorPiece] = None
right_hand: Optional[ArmorPiece] = None
legs: Optional[ArmorPiece] = None
feet: Optional[ArmorPiece] = None
def get_all_pieces(self) -> list:
"""Get list of all equipped pieces."""
pieces = []
for piece in [self.head, self.chest, self.left_arm, self.right_arm,
self.left_hand, self.right_hand, self.legs, self.feet]:
if piece:
pieces.append(piece)
return pieces
def get_total_protection(self, damage_type: str = "impact") -> Decimal:
"""Get total protection for a damage type."""
total = Decimal("0")
for piece in self.get_all_pieces():
protection = getattr(piece, f"protection_{damage_type}", Decimal("0"))
total += protection
return total
def calculate_total_decay(self, damage_per_piece: Dict[str, Decimal]) -> Decimal:
"""Calculate total decay for all pieces.
Args:
damage_per_piece: Dict mapping slot names to damage absorbed
Returns:
Total decay in PED
"""
total_decay = Decimal("0")
for piece in self.get_all_pieces():
if piece.slot in damage_per_piece:
damage = damage_per_piece[piece.slot]
total_decay += piece.calculate_decay(damage)
return total_decay
def get_armor_durability(armor_name: str) -> int:
"""Get durability for an armor by name.
Args:
armor_name: Name of the armor
Returns:
Durability value (defaults to 2000 if unknown)
"""
return ARMOR_DURABILITY.get(armor_name, DEFAULT_DURABILITY)
def compare_armor_economy(armor_names: list) -> list:
"""Compare economy of multiple armors.
Args:
armor_names: List of armor names to compare
Returns:
List of tuples (name, durability, hp_per_pec, dmg_per_100ped)
"""
results = []
for name in armor_names:
durability = get_armor_durability(name)
hp_per_pec = calculate_hp_per_pec(durability)
dmg_per_100 = calculate_protection_cost_per_100_ped(durability)
results.append((name, durability, hp_per_pec, dmg_per_100))
# Sort by economy (hp/pec) descending
results.sort(key=lambda x: x[2], reverse=True)
return results
# Example usage
if __name__ == "__main__":
# Compare popular armors
armors = ["Ghost", "Gremlin", "Adjusted Nemesis", "Angel",
"Martial (L)", "Angel (L)", "Perseus (L)"]
print("Armor Economy Comparison:")
print("-" * 70)
print(f"{'Armor':<25} {'Dur':<8} {'hp/pec':<12} {'dmg/100PED':<12}")
print("-" * 70)
for name, durability, hp_per_pec, dmg_per_100 in compare_armor_economy(armors):
print(f"{name:<25} {durability:<8} {hp_per_pec:<12.2f} {dmg_per_100:<12,}")
# Example decay calculation
print("\n\nDecay Example (15 damage absorbed):")
print("-" * 50)
for armor in ["Ghost", "Angel", "Martial (L)"]:
durability = get_armor_durability(armor)
decay = calculate_armor_decay(Decimal("15"), durability)
print(f"{armor:<20} {decay:.5f} PED")

View File

@ -3,9 +3,9 @@
# Standards: Python 3.11+, type hints, Decimal precision for PED/PEC # Standards: Python 3.11+, type hints, Decimal precision for PED/PEC
from decimal import Decimal from decimal import Decimal
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict, field
import json import json
import logging import logging
@ -45,6 +45,204 @@ class SessionData:
notes: str = "" notes: str = ""
@dataclass
class HuntingSessionData:
"""
Extended data class for hunting sessions with detailed metrics.
This class tracks all hunting-specific statistics including:
- Loot breakdown (shrapnel, universal ammo, other loot)
- Cost breakdown (weapon, armor, healing, plates)
- Combat statistics (damage, kills, shots fired)
- Special events (globals, HoFs)
- Calculated efficiency metrics (DPP, return %, cost/kill)
"""
id: Optional[int] = None
session_id: Optional[int] = None
# Timestamps
started_at: Optional[datetime] = None
ended_at: Optional[datetime] = None
# Loot tracking (PED values)
total_loot_ped: Decimal = Decimal("0.0")
total_shrapnel_ped: Decimal = Decimal("0.0")
total_universal_ammo_ped: Decimal = Decimal("0.0")
total_other_loot_ped: Decimal = Decimal("0.0") # Actual marketable loot
# Cost tracking (PED values)
weapon_cost_ped: Decimal = Decimal("0.0")
armor_cost_ped: Decimal = Decimal("0.0")
healing_cost_ped: Decimal = Decimal("0.0")
plates_cost_ped: Decimal = Decimal("0.0")
enhancer_cost_ped: Decimal = Decimal("0.0")
total_cost_ped: Decimal = Decimal("0.0")
# Combat tracking
damage_dealt: Decimal = Decimal("0.0")
damage_taken: Decimal = Decimal("0.0")
healing_done: Decimal = Decimal("0.0")
shots_fired: int = 0
shots_missed: int = 0
evades: int = 0
kills: int = 0
# Special events
globals_count: int = 0
hofs_count: int = 0
personal_globals: List[Dict[str, Any]] = field(default_factory=list)
# Skill gains (tracked separately)
skill_gains: Dict[str, Decimal] = field(default_factory=dict)
# Equipment used
weapon_name: str = ""
weapon_dpp: Decimal = Decimal("0.0")
armor_name: str = ""
fap_name: str = ""
# Calculated properties
@property
def net_profit_ped(self) -> Decimal:
"""Net profit (excluding shrapnel from loot value)."""
return self.total_other_loot_ped - self.total_cost_ped
@property
def return_percentage(self) -> Decimal:
"""Return percentage (other_loot / cost * 100)."""
if self.total_cost_ped > 0:
return (self.total_other_loot_ped / self.total_cost_ped) * Decimal('100')
return Decimal('0.0')
@property
def return_percentage_with_shrapnel(self) -> Decimal:
"""Return percentage including shrapnel."""
if self.total_cost_ped > 0:
return (self.total_loot_ped / self.total_cost_ped) * Decimal('100')
return Decimal('0.0')
@property
def cost_per_kill(self) -> Decimal:
"""Average cost per kill."""
if self.kills > 0:
return self.total_cost_ped / self.kills
return Decimal('0.0')
@property
def cost_per_damage(self) -> Decimal:
"""Cost per damage point (PED/damage)."""
if self.damage_dealt > 0:
return self.total_cost_ped / self.damage_dealt
return Decimal('0.0')
@property
def dpp(self) -> Decimal:
"""Damage Per PED - efficiency metric."""
if self.total_cost_ped > 0:
return self.damage_dealt / self.total_cost_ped
return Decimal('0.0')
@property
def damage_per_kill(self) -> Decimal:
"""Average damage per kill."""
if self.kills > 0:
return self.damage_dealt / self.kills
return Decimal('0.0')
@property
def loot_per_kill(self) -> Decimal:
"""Average loot per kill."""
if self.kills > 0:
return self.total_other_loot_ped / self.kills
return Decimal('0.0')
@property
def accuracy(self) -> Decimal:
"""Hit accuracy percentage."""
total_shots = self.shots_fired + self.shots_missed
if total_shots > 0:
return (self.shots_fired / total_shots) * Decimal('100')
return Decimal('0.0')
@property
def session_duration(self) -> Optional[timedelta]:
"""Calculate session duration."""
if self.started_at and self.ended_at:
return self.ended_at - self.started_at
elif self.started_at:
return datetime.now() - self.started_at
return None
@property
def kills_per_hour(self) -> Decimal:
"""Kill rate per hour."""
duration = self.session_duration
if duration and duration.total_seconds() > 0:
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
return Decimal(str(self.kills)) / hours
return Decimal('0.0')
@property
def cost_per_hour(self) -> Decimal:
"""Cost rate per hour."""
duration = self.session_duration
if duration and duration.total_seconds() > 0:
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
return self.total_cost_ped / hours
return Decimal('0.0')
@property
def profit_per_hour(self) -> Decimal:
"""Profit rate per hour."""
duration = self.session_duration
if duration and duration.total_seconds() > 0:
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
return self.net_profit_ped / hours
return Decimal('0.0')
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
'id': self.id,
'session_id': self.session_id,
'started_at': self.started_at.isoformat() if self.started_at else None,
'ended_at': self.ended_at.isoformat() if self.ended_at else None,
'total_loot_ped': float(self.total_loot_ped),
'total_shrapnel_ped': float(self.total_shrapnel_ped),
'total_universal_ammo_ped': float(self.total_universal_ammo_ped),
'total_other_loot_ped': float(self.total_other_loot_ped),
'weapon_cost_ped': float(self.weapon_cost_ped),
'armor_cost_ped': float(self.armor_cost_ped),
'healing_cost_ped': float(self.healing_cost_ped),
'plates_cost_ped': float(self.plates_cost_ped),
'enhancer_cost_ped': float(self.enhancer_cost_ped),
'total_cost_ped': float(self.total_cost_ped),
'damage_dealt': float(self.damage_dealt),
'damage_taken': float(self.damage_taken),
'healing_done': float(self.healing_done),
'shots_fired': self.shots_fired,
'shots_missed': self.shots_missed,
'evades': self.evades,
'kills': self.kills,
'globals_count': self.globals_count,
'hofs_count': self.hofs_count,
'net_profit_ped': float(self.net_profit_ped),
'return_percentage': float(self.return_percentage),
'return_percentage_with_shrapnel': float(self.return_percentage_with_shrapnel),
'cost_per_kill': float(self.cost_per_kill),
'dpp': float(self.dpp),
'damage_per_kill': float(self.damage_per_kill),
'loot_per_kill': float(self.loot_per_kill),
'kills_per_hour': float(self.kills_per_hour),
'cost_per_hour': float(self.cost_per_hour),
'profit_per_hour': float(self.profit_per_hour),
'weapon_name': self.weapon_name,
'weapon_dpp': float(self.weapon_dpp),
'armor_name': self.armor_name,
'fap_name': self.fap_name,
}
@dataclass @dataclass
class LootEvent: class LootEvent:
"""Data class representing a loot event from chat.log.""" """Data class representing a loot event from chat.log."""
@ -55,12 +253,28 @@ class LootEvent:
item_name: Optional[str] = None item_name: Optional[str] = None
quantity: int = 1 quantity: int = 1
value_ped: Decimal = Decimal("0.0") value_ped: Decimal = Decimal("0.0")
is_shrapnel: bool = False
is_universal_ammo: bool = False
creature_name: Optional[str] = None creature_name: Optional[str] = None
zone_name: Optional[str] = None zone_name: Optional[str] = None
raw_log_line: Optional[str] = None raw_log_line: Optional[str] = None
screenshot_path: Optional[str] = None screenshot_path: Optional[str] = None
@dataclass
class CombatEvent:
"""Data class representing a combat event."""
id: Optional[int] = None
session_id: Optional[int] = None
timestamp: Optional[datetime] = None
event_type: str = "damage_dealt" # damage_dealt, damage_taken, heal, evade, kill
damage_amount: Optional[Decimal] = None
heal_amount: Optional[Decimal] = None
creature_name: Optional[str] = None
weapon_name: Optional[str] = None
raw_log_line: Optional[str] = None
class ProjectManager: class ProjectManager:
""" """
Manages Projects implementing the Data Principle. Manages Projects implementing the Data Principle.
@ -74,6 +288,7 @@ class ProjectManager:
db: DatabaseManager instance for persistence db: DatabaseManager instance for persistence
current_project: Currently active ProjectData current_project: Currently active ProjectData
current_session: Currently active SessionData current_session: Currently active SessionData
current_hunting_session: Currently active HuntingSessionData
""" """
def __init__(self, db: Optional[DatabaseManager] = None): def __init__(self, db: Optional[DatabaseManager] = None):
@ -86,6 +301,7 @@ class ProjectManager:
self.db = db if db else DatabaseManager() self.db = db if db else DatabaseManager()
self.current_project: Optional[ProjectData] = None self.current_project: Optional[ProjectData] = None
self.current_session: Optional[SessionData] = None self.current_session: Optional[SessionData] = None
self.current_hunting_session: Optional[HuntingSessionData] = None
logger.info("ProjectManager initialized") logger.info("ProjectManager initialized")
@ -270,6 +486,62 @@ class ProjectManager:
logger.info(f"Started session {session.id} for project {project_id}") logger.info(f"Started session {session.id} for project {project_id}")
return session return session
def start_hunting_session(self, project_id: int,
weapon_name: str = "",
weapon_dpp: Decimal = Decimal('0.0'),
armor_name: str = "",
fap_name: str = "") -> HuntingSessionData:
"""
Start a new hunting session with full tracking.
Args:
project_id: Parent project ID
weapon_name: Name of equipped weapon
weapon_dpp: Weapon DPP rating
armor_name: Name of equipped armor
fap_name: Name of equipped FAP
Returns:
Created HuntingSessionData
"""
# First create base session
base_session = self.start_session(project_id, notes=f"Hunting with {weapon_name}")
# Create hunting session data
hunting_session = HuntingSessionData(
session_id=base_session.id,
started_at=datetime.now(),
weapon_name=weapon_name,
weapon_dpp=weapon_dpp,
armor_name=armor_name,
fap_name=fap_name
)
self.current_hunting_session = hunting_session
# Insert into hunting_sessions table
cursor = self.db.execute(
"""
INSERT INTO hunting_sessions
(session_id, started_at, weapon_name, weapon_dpp, armor_name, fap_name)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
base_session.id,
hunting_session.started_at,
weapon_name,
float(weapon_dpp),
armor_name,
fap_name
)
)
self.db.commit()
hunting_session.id = cursor.lastrowid
logger.info(f"Started hunting session {hunting_session.id} with {weapon_name}")
return hunting_session
def end_session(self, session_id: Optional[int] = None) -> bool: def end_session(self, session_id: Optional[int] = None) -> bool:
""" """
End a session and calculate final metrics. End a session and calculate final metrics.
@ -287,6 +559,10 @@ class ProjectManager:
logger.error("No session ID provided") logger.error("No session ID provided")
return False return False
# If we have an active hunting session, finalize it first
if self.current_hunting_session and self.current_hunting_session.session_id == session_id:
self._finalize_hunting_session()
# Calculate metrics # Calculate metrics
cursor = self.db.execute( cursor = self.db.execute(
""" """
@ -331,6 +607,115 @@ class ProjectManager:
logger.info(f"Ended session {session_id}. Profit: {net_profit} PED") logger.info(f"Ended session {session_id}. Profit: {net_profit} PED")
return True return True
def _finalize_hunting_session(self):
"""Finalize the current hunting session in the database."""
if not self.current_hunting_session:
return
hs = self.current_hunting_session
hs.ended_at = datetime.now()
self.db.execute(
"""
UPDATE hunting_sessions SET
ended_at = ?,
total_loot_ped = ?,
total_shrapnel_ped = ?,
total_universal_ammo_ped = ?,
total_other_loot_ped = ?,
weapon_cost_ped = ?,
armor_cost_ped = ?,
healing_cost_ped = ?,
plates_cost_ped = ?,
total_cost_ped = ?,
damage_dealt = ?,
damage_taken = ?,
healing_done = ?,
shots_fired = ?,
shots_missed = ?,
evades = ?,
kills = ?,
globals_count = ?,
hofs_count = ?
WHERE id = ?
""",
(
hs.ended_at,
float(hs.total_loot_ped),
float(hs.total_shrapnel_ped),
float(hs.total_universal_ammo_ped),
float(hs.total_other_loot_ped),
float(hs.weapon_cost_ped),
float(hs.armor_cost_ped),
float(hs.healing_cost_ped),
float(hs.plates_cost_ped),
float(hs.total_cost_ped),
float(hs.damage_dealt),
float(hs.damage_taken),
float(hs.healing_done),
hs.shots_fired,
hs.shots_missed,
hs.evades,
hs.kills,
hs.globals_count,
hs.hofs_count,
hs.id
)
)
self.db.commit()
# Store personal globals
for global_data in hs.personal_globals:
self.db.execute(
"""
INSERT INTO hunting_globals
(hunting_session_id, timestamp, creature_name, value_ped, is_hof)
VALUES (?, ?, ?, ?, ?)
""",
(
hs.id,
global_data.get('timestamp', datetime.now()),
global_data.get('creature', 'Unknown'),
float(global_data.get('value_ped', Decimal('0.0'))),
global_data.get('is_hof', False)
)
)
self.db.commit()
logger.info(f"Finalized hunting session {hs.id}")
def update_hunting_session(self, updates: Dict[str, Any]) -> bool:
"""
Update the current hunting session with new data.
Args:
updates: Dictionary of field updates
Returns:
True if updated successfully
"""
if not self.current_hunting_session:
logger.error("No active hunting session to update")
return False
hs = self.current_hunting_session
# Update fields
for key, value in updates.items():
if hasattr(hs, key):
setattr(hs, key, value)
# Recalculate totals
hs.total_cost_ped = (
hs.weapon_cost_ped +
hs.armor_cost_ped +
hs.healing_cost_ped +
hs.plates_cost_ped +
hs.enhancer_cost_ped
)
return True
# ======================================================================== # ========================================================================
# LOOT TRACKING # LOOT TRACKING
# ======================================================================== # ========================================================================
@ -380,6 +765,44 @@ class ProjectManager:
return True return True
def record_combat_event(self, event: CombatEvent) -> bool:
"""
Record a combat event.
Args:
event: CombatEvent data
Returns:
True if recorded successfully
"""
if not self.current_session:
logger.error("No active session to record combat event")
return False
event.session_id = self.current_session.id
self.db.execute(
"""
INSERT INTO combat_events
(session_id, timestamp, event_type, damage_amount, heal_amount,
creature_name, weapon_name, raw_log_line)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
event.session_id,
event.timestamp or datetime.now(),
event.event_type,
float(event.damage_amount) if event.damage_amount else None,
float(event.heal_amount) if event.heal_amount else None,
event.creature_name,
event.weapon_name,
event.raw_log_line
)
)
self.db.commit()
return True
def _trigger_screenshot(self, loot: LootEvent) -> None: def _trigger_screenshot(self, loot: LootEvent) -> None:
""" """
Trigger screenshot capture for high-value loot. Trigger screenshot capture for high-value loot.
@ -426,6 +849,99 @@ class ProjectManager:
'hof_count': row['hof_count'] 'hof_count': row['hof_count']
} }
def get_hunting_session_summary(self, hunting_session_id: int) -> Optional[Dict[str, Any]]:
"""
Get detailed summary for a hunting session.
Args:
hunting_session_id: Hunting session ID
Returns:
Dictionary with hunting session summary
"""
cursor = self.db.execute(
"SELECT * FROM hunting_sessions WHERE id = ?",
(hunting_session_id,)
)
row = cursor.fetchone()
if not row:
return None
# Build hunting session data
hs = HuntingSessionData(
id=row['id'],
session_id=row['session_id'],
started_at=row['started_at'],
ended_at=row['ended_at'],
total_loot_ped=Decimal(str(row['total_loot_ped'])),
total_shrapnel_ped=Decimal(str(row['total_shrapnel_ped'])),
total_universal_ammo_ped=Decimal(str(row['total_universal_ammo_ped'])),
total_other_loot_ped=Decimal(str(row['total_other_loot_ped'])),
weapon_cost_ped=Decimal(str(row['weapon_cost_ped'])),
armor_cost_ped=Decimal(str(row['armor_cost_ped'])),
healing_cost_ped=Decimal(str(row['healing_cost_ped'])),
plates_cost_ped=Decimal(str(row['plates_cost_ped'])),
total_cost_ped=Decimal(str(row['total_cost_ped'])),
damage_dealt=Decimal(str(row['damage_dealt'])),
damage_taken=Decimal(str(row['damage_taken'])),
healing_done=Decimal(str(row['healing_done'])),
shots_fired=row['shots_fired'],
shots_missed=row['shots_missed'],
evades=row['evades'],
kills=row['kills'],
globals_count=row['globals_count'],
hofs_count=row['hofs_count'],
weapon_name=row['weapon_name'],
weapon_dpp=Decimal(str(row['weapon_dpp'])),
armor_name=row['armor_name'],
fap_name=row['fap_name']
)
return hs.to_dict()
def get_hunting_history(self, project_id: int, limit: int = 10) -> List[Dict[str, Any]]:
"""
Get hunting session history for a project.
Args:
project_id: Project ID
limit: Maximum number of sessions to return
Returns:
List of hunting session summaries
"""
cursor = self.db.execute(
"""
SELECT hs.* FROM hunting_sessions hs
JOIN sessions s ON hs.session_id = s.id
WHERE s.project_id = ?
ORDER BY hs.started_at DESC
LIMIT ?
""",
(project_id, limit)
)
rows = cursor.fetchall()
history = []
for row in rows:
hs = HuntingSessionData(
id=row['id'],
session_id=row['session_id'],
started_at=row['started_at'],
ended_at=row['ended_at'],
total_loot_ped=Decimal(str(row['total_loot_ped'])),
total_other_loot_ped=Decimal(str(row['total_other_loot_ped'])),
total_cost_ped=Decimal(str(row['total_cost_ped'])),
kills=row['kills'],
globals_count=row['globals_count'],
hofs_count=row['hofs_count'],
weapon_name=row['weapon_name']
)
history.append(hs.to_dict())
return history
def compare_to_historical(self, project_id: int, def compare_to_historical(self, project_id: int,
metric: str = 'net_profit') -> Dict[str, Any]: metric: str = 'net_profit') -> Dict[str, Any]:
""" """
@ -485,5 +1001,7 @@ __all__ = [
'ProjectManager', 'ProjectManager',
'ProjectData', 'ProjectData',
'SessionData', 'SessionData',
'LootEvent' 'HuntingSessionData',
'LootEvent',
'CombatEvent'
] ]

View File

@ -1,7 +1,8 @@
-- Description: SQLite database schema for Lemontropia Suite -- Description: SQLite database schema for Lemontropia Suite
-- Implements the Data Principle: Every session is a Project -- Implements the Data Principle: Every session is a Project
-- Schema version: 1.0.0 -- Schema version: 2.0.0 - Added comprehensive hunting session tracking
-- Created: 2026-02-08 -- Created: 2026-02-08
-- Updated: 2026-02-09
-- Enable foreign key support -- Enable foreign key support
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
@ -45,6 +46,73 @@ CREATE TABLE IF NOT EXISTS sessions (
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at); CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
-- ============================================================================
-- HUNTING SESSIONS (Extended tracking for hunting activities)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hunting_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL UNIQUE,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
-- Loot breakdown
total_loot_ped REAL DEFAULT 0.0,
total_shrapnel_ped REAL DEFAULT 0.0,
total_universal_ammo_ped REAL DEFAULT 0.0,
total_other_loot_ped REAL DEFAULT 0.0, -- Marketable loot excluding shrapnel/UA
-- Cost breakdown
weapon_cost_ped REAL DEFAULT 0.0,
armor_cost_ped REAL DEFAULT 0.0,
healing_cost_ped REAL DEFAULT 0.0,
plates_cost_ped REAL DEFAULT 0.0,
enhancer_cost_ped REAL DEFAULT 0.0,
total_cost_ped REAL DEFAULT 0.0,
-- Combat statistics
damage_dealt REAL DEFAULT 0.0,
damage_taken REAL DEFAULT 0.0,
healing_done REAL DEFAULT 0.0,
shots_fired INTEGER DEFAULT 0,
shots_missed INTEGER DEFAULT 0,
evades INTEGER DEFAULT 0,
kills INTEGER DEFAULT 0,
-- Special events
globals_count INTEGER DEFAULT 0,
hofs_count INTEGER DEFAULT 0,
-- Equipment used
weapon_name TEXT,
weapon_dpp REAL DEFAULT 0.0,
armor_name TEXT,
fap_name TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_hunting_sessions_session ON hunting_sessions(session_id);
CREATE INDEX IF NOT EXISTS idx_hunting_sessions_weapon ON hunting_sessions(weapon_name);
-- ============================================================================
-- HUNTING GLOBALS/HoFs (Record of notable loot events)
-- ============================================================================
CREATE TABLE IF NOT EXISTS hunting_globals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hunting_session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
creature_name TEXT,
value_ped REAL NOT NULL,
is_hof BOOLEAN DEFAULT 0,
screenshot_path TEXT,
FOREIGN KEY (hunting_session_id) REFERENCES hunting_sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_hunting_globals_session ON hunting_globals(hunting_session_id);
CREATE INDEX IF NOT EXISTS idx_hunting_globals_value ON hunting_globals(value_ped);
-- ============================================================================ -- ============================================================================
-- LOOT EVENTS (Core data capture) -- LOOT EVENTS (Core data capture)
-- ============================================================================ -- ============================================================================
@ -59,6 +127,8 @@ CREATE TABLE IF NOT EXISTS loot_events (
item_name TEXT, item_name TEXT,
quantity INTEGER DEFAULT 1, quantity INTEGER DEFAULT 1,
value_ped REAL DEFAULT 0.0, value_ped REAL DEFAULT 0.0,
is_shrapnel BOOLEAN DEFAULT 0,
is_universal_ammo BOOLEAN DEFAULT 0,
-- Context -- Context
creature_name TEXT, -- For hunter module creature_name TEXT, -- For hunter module
@ -78,6 +148,29 @@ CREATE INDEX IF NOT EXISTS idx_loot_timestamp ON loot_events(timestamp);
CREATE INDEX IF NOT EXISTS idx_loot_type ON loot_events(event_type); CREATE INDEX IF NOT EXISTS idx_loot_type ON loot_events(event_type);
CREATE INDEX IF NOT EXISTS idx_loot_value ON loot_events(value_ped); CREATE INDEX IF NOT EXISTS idx_loot_value ON loot_events(value_ped);
-- ============================================================================
-- COMBAT EVENTS (Detailed combat tracking)
-- ============================================================================
CREATE TABLE IF NOT EXISTS combat_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
event_type TEXT NOT NULL CHECK (event_type IN ('damage_dealt', 'damage_taken', 'heal', 'evade', 'kill', 'critical_hit')),
damage_amount REAL,
heal_amount REAL,
creature_name TEXT,
weapon_name TEXT,
raw_log_line TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_combat_session ON combat_events(session_id);
CREATE INDEX IF NOT EXISTS idx_combat_type ON combat_events(event_type);
-- ============================================================================ -- ============================================================================
-- SKILL GAINS (Character progression tracking) -- SKILL GAINS (Character progression tracking)
-- ============================================================================ -- ============================================================================
@ -106,6 +199,7 @@ CREATE TABLE IF NOT EXISTS decay_events (
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
item_name TEXT NOT NULL, item_name TEXT NOT NULL,
decay_amount_ped REAL DEFAULT 0.0, decay_amount_ped REAL DEFAULT 0.0,
decay_amount_pec REAL DEFAULT 0.0,
shots_fired INTEGER DEFAULT 0, shots_fired INTEGER DEFAULT 0,
raw_log_line TEXT, raw_log_line TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
@ -149,9 +243,9 @@ CREATE TABLE IF NOT EXISTS schema_version (
description TEXT description TEXT
); );
-- Insert initial version -- Insert/update version
INSERT OR IGNORE INTO schema_version (version, description) INSERT OR REPLACE INTO schema_version (version, description)
VALUES (1, 'Initial schema with Data Principle support'); VALUES (2, 'Added comprehensive hunting session tracking with loot/cost breakdown');
-- ============================================================================ -- ============================================================================
-- VIEWS FOR COMMON QUERIES (Performance optimization) -- VIEWS FOR COMMON QUERIES (Performance optimization)
@ -199,3 +293,56 @@ FROM sessions s
JOIN projects p ON s.project_id = p.id JOIN projects p ON s.project_id = p.id
LEFT JOIN loot_events l ON s.id = l.session_id LEFT JOIN loot_events l ON s.id = l.session_id
GROUP BY s.id; GROUP BY s.id;
-- Hunting session detailed view
CREATE VIEW IF NOT EXISTS v_hunting_session_summary AS
SELECT
hs.id,
hs.session_id,
s.project_id,
p.name as project_name,
hs.started_at,
hs.ended_at,
hs.total_loot_ped,
hs.total_other_loot_ped,
hs.total_cost_ped,
hs.damage_dealt,
hs.kills,
hs.globals_count,
hs.hofs_count,
hs.weapon_name,
hs.weapon_dpp,
CASE
WHEN hs.total_cost_ped > 0
THEN ROUND((hs.total_other_loot_ped / hs.total_cost_ped) * 100, 2)
ELSE 0
END as return_percent,
CASE
WHEN hs.kills > 0
THEN ROUND(hs.total_cost_ped / hs.kills, 4)
ELSE 0
END as cost_per_kill,
CASE
WHEN hs.total_cost_ped > 0
THEN ROUND(hs.damage_dealt / hs.total_cost_ped, 2)
ELSE 0
END as dpp
FROM hunting_sessions hs
JOIN sessions s ON hs.session_id = s.id
JOIN projects p ON s.project_id = p.id;
-- Weapon performance analysis
CREATE VIEW IF NOT EXISTS v_weapon_performance AS
SELECT
weapon_name,
COUNT(*) as sessions_count,
AVG(total_other_loot_ped) as avg_loot,
AVG(total_cost_ped) as avg_cost,
AVG(CASE WHEN total_cost_ped > 0 THEN (total_other_loot_ped / total_cost_ped) * 100 ELSE 0 END) as avg_return_percent,
AVG(dpp) as avg_dpp,
SUM(kills) as total_kills,
SUM(globals_count) as total_globals,
SUM(hofs_count) as total_hofs
FROM hunting_sessions
WHERE weapon_name IS NOT NULL AND weapon_name != ''
GROUP BY weapon_name;

167
docs/ARMOR_DECAY_FORMULA.md Normal file
View File

@ -0,0 +1,167 @@
# Entropia Universe Armor Decay Formula
**Source:** "A Most Complete Guide to Armors (2020 Edition)"
**Formula Author:** Hijacker27 (confirmed through testing)
**Date:** 2026-02-09
---
## Armor Decay Formula (VU 15.15)
```
Decay (in PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
```
Where:
- **damage_absorbed** = Amount of damage the armor piece absorbed
- **durability** = Armor's durability stat (varies by armor type)
- **Decay output is in PEC** (divide by 100 for PED)
---
## Armor Economy (hp/pec)
The economy can be calculated as:
```
hp/pec = 20 / (1 - durability/100000)
```
Or simplified:
```
hp/pec ≈ 20 + (durability/5000)
```
---
## Unlimited Armor Protection Costs
| Armor | Durability | hp/pec | dmg/100 PED decay |
|-------|------------|--------|-------------------|
| Ghost | 2000 | ~20.41 | 204,082 damage |
| Gremlin | 2950 | ~20.61 | 206,079 damage |
| Adjusted Nemesis | 3400 | ~20.70 | 207,039 damage |
| Angel | 4000 | ~20.83 | 208,333 damage |
**Analysis:** Using Angel instead of Ghost saves ~2 PED for every 100 PED of decay (2% savings).
---
## Limited (L) Armor Protection Costs
**Note:** Formula may differ for Limited armors with >10k durability (research ongoing).
| Armor | Durability | hp/pec | dmg/100 PED | vs Ghost |
|-------|------------|--------|-------------|----------|
| Martial (L) | 13,000 | ~22.99 | 229,885 | ⮟ 11.22% less decay |
| Mayhem (L) | 13,300 | ~23.07 | 230,680 | ⮟ 11.53% less decay |
| Angel (L) | 14,000 | ~23.26 | 232,558 | ⮟ 12.24% less decay |
| Perseus (L) | 15,000 | ~23.53 | 235,294 | ⮟ 13.27% less decay |
| Moonshine (L) | 15,400 | ~23.64 | 236,407 | ⮟ 13.67% less decay |
**Key Insight:** Limited armors with high durability (13k+) have significantly better economy than unlimited armors.
---
## Calculation Examples
### Example 1: Ghost Armor (2000 durability)
Monster hits for 15 Impact, armor absorbs all 15:
```
Decay = 15 * 0.05 * (1 - 2000/100000)
Decay = 15 * 0.05 * 0.98
Decay = 0.735 PEC
Decay = 0.00735 PED
```
### Example 2: Angel Armor (4000 durability)
Same 15 Impact hit:
```
Decay = 15 * 0.05 * (1 - 4000/100000)
Decay = 15 * 0.05 * 0.96
Decay = 0.72 PEC
Decay = 0.0072 PED
```
**Savings:** 0.015 PEC (2% less decay than Ghost)
### Example 3: Martial (L) (13000 durability)
Same 15 Impact hit:
```
Decay = 15 * 0.05 * (1 - 13000/100000)
Decay = 15 * 0.05 * 0.87
Decay = 0.6525 PEC
Decay = 0.006525 PED
```
**Savings:** 0.0825 PEC (11.22% less decay than Ghost)
---
## Implementation for Lemontropia Suite
### Python Implementation:
```python
from decimal import Decimal
def calculate_armor_decay(damage_absorbed: Decimal, durability: int) -> Decimal:
"""Calculate armor decay in PED.
Args:
damage_absorbed: Amount of damage absorbed by armor
durability: Armor durability stat
Returns:
Decay cost in PED
"""
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
decay_pec = damage_absorbed * Decimal("0.05") * durability_factor
return decay_pec / Decimal(100) # Convert PEC to PED
# Example usage
decay = calculate_armor_decay(Decimal("15"), 2000) # Ghost absorbing 15 dmg
print(f"Decay: {decay:.5f} PED") # 0.00735 PED
```
### Armor Stats Database:
```python
ARMOR_DURABILITY = {
"Ghost": 2000,
"Gremlin": 2950,
"Adjusted Nemesis": 3400,
"Angel": 4000,
"Martial (L)": 13000,
"Mayhem (L)": 13300,
"Angel (L)": 14000,
"Perseus (L)": 15000,
"Moonshine (L)": 15400,
}
```
---
## Plate Decay Formula
**Note:** Plate decay uses the same formula with plate's own durability.
```
Plate Decay = damage_absorbed_by_plate * 0.05 * (1 - plate_durability/100000)
```
**Important:** Plate and armor decay are calculated **independently** based on how much damage each absorbed.
---
## Research Notes
1. **Formula confirmed** by Hijacker27 and guide author through independent testing
2. **Limited armors >10k durability** may use different formula (needs more research)
3. **Decay is linear** per damage point absorbed (post-Loot 2.0)
4. **No minimum decay** - always proportional to damage absorbed
---
## References
- PlanetCalypsoForum: "A Most Complete Guide to Armors (2020 Edition)"
- Hijacker27's research on armor decay
- VU 15.15 patch notes (Loot 2.0 armor changes)

View File

@ -178,6 +178,16 @@ class GearSelectorDialog(QDialog):
header.resizeSection(2, 80) header.resizeSection(2, 80)
header.resizeSection(3, 80) header.resizeSection(3, 80)
header.resizeSection(4, 80) header.resizeSection(4, 80)
elif self.gear_type == "medical_tool":
self.results_tree.setHeaderLabels(["Name", "Max Heal", "Decay", "Cost/Heal", "Cost/h"])
header = self.results_tree.header()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
for i in range(1, 5):
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
header.resizeSection(1, 80)
header.resizeSection(2, 80)
header.resizeSection(3, 80)
header.resizeSection(4, 80)
def load_data_async(self): def load_data_async(self):
"""Load data in background thread.""" """Load data in background thread."""
@ -193,6 +203,10 @@ class GearSelectorDialog(QDialog):
self.loader_thread = FinderLoaderThread() self.loader_thread = FinderLoaderThread()
self.loader_thread.finders_loaded.connect(self.on_data_loaded) self.loader_thread.finders_loaded.connect(self.on_data_loaded)
self.loader_thread.error_occurred.connect(self.on_load_error) self.loader_thread.error_occurred.connect(self.on_load_error)
elif self.gear_type == "medical_tool":
self.loader_thread = MedicalToolLoaderThread()
self.loader_thread.medical_tools_loaded.connect(self.on_data_loaded)
self.loader_thread.error_occurred.connect(self.on_load_error)
else: else:
return return
@ -246,6 +260,17 @@ class GearSelectorDialog(QDialog):
]) ])
item.setData(0, Qt.ItemDataRole.UserRole, f) item.setData(0, Qt.ItemDataRole.UserRole, f)
self.results_tree.addTopLevelItem(item) self.results_tree.addTopLevelItem(item)
elif self.gear_type == "medical_tool":
for m in items:
item = QTreeWidgetItem([
m.name,
str(m.max_heal) if m.max_heal else "-",
f"{m.decay:.2f}" if m.decay else "-",
f"{m.cost_per_heal:.4f}" if m.cost_per_heal else "-",
f"{m.cost_per_hour:.2f}" if m.cost_per_hour else "-"
])
item.setData(0, Qt.ItemDataRole.UserRole, m)
self.results_tree.addTopLevelItem(item)
def on_search(self): def on_search(self):
"""Search items.""" """Search items."""
@ -296,6 +321,15 @@ class GearSelectorDialog(QDialog):
self.stats_layout.addRow("Depth:", QLabel(f"{item.depth}m")) self.stats_layout.addRow("Depth:", QLabel(f"{item.depth}m"))
self.stats_layout.addRow("Radius:", QLabel(f"{item.radius}m")) self.stats_layout.addRow("Radius:", QLabel(f"{item.radius}m"))
self.stats_layout.addRow("Decay:", QLabel(f"{item.decay} PEC")) self.stats_layout.addRow("Decay:", QLabel(f"{item.decay} PEC"))
elif self.gear_type == "medical_tool" and isinstance(item, MedicalTool):
self.stats_layout.addRow("Name:", QLabel(item.name))
self.stats_layout.addRow("Max Heal:", QLabel(f"{item.max_heal} HP" if item.max_heal else "-"))
self.stats_layout.addRow("Min Heal:", QLabel(f"{item.min_heal} HP" if item.min_heal else "-"))
self.stats_layout.addRow("Decay:", QLabel(f"{item.decay:.2f} PEC/use" if item.decay else "-"))
self.stats_layout.addRow("Cost/Heal:", QLabel(f"{item.cost_per_heal:.4f} PED" if item.cost_per_heal else "-"))
self.stats_layout.addRow("Cost/Hour:", QLabel(f"{item.cost_per_hour:.2f} PED" if item.cost_per_hour else "-"))
if item.uses_per_minute:
self.stats_layout.addRow("Uses/Min:", QLabel(str(item.uses_per_minute)))
def on_item_double_clicked(self, item, column): def on_item_double_clicked(self, item, column):
"""Handle double click.""" """Handle double click."""
@ -325,6 +359,18 @@ class GearSelectorDialog(QDialog):
self.gear_selected.emit("finder", f.name, { self.gear_selected.emit("finder", f.name, {
'id': f.id, 'item_id': f.item_id, 'depth': float(f.depth), 'radius': float(f.radius), 'id': f.id, 'item_id': f.item_id, 'depth': float(f.depth), 'radius': float(f.radius),
}) })
elif self.gear_type == "medical_tool":
m = self.selected_gear
self.gear_selected.emit("medical_tool", m.name, {
'id': m.id,
'item_id': m.item_id,
'max_heal': float(m.max_heal) if m.max_heal else 0,
'min_heal': float(m.min_heal) if m.min_heal else 0,
'decay': float(m.decay) if m.decay else 0,
'cost_per_heal': float(m.cost_per_heal) if m.cost_per_heal else 0,
'cost_per_hour': float(m.cost_per_hour) if m.cost_per_hour else 0,
'uses_per_minute': m.uses_per_minute or 0,
})
self.accept() self.accept()

View File

@ -857,6 +857,7 @@ class LoadoutManagerDialog(QDialog):
"""Main dialog for managing hunting loadouts with full API integration.""" """Main dialog for managing hunting loadouts with full API integration."""
loadout_saved = pyqtSignal(str) # Emitted when loadout is saved loadout_saved = pyqtSignal(str) # Emitted when loadout is saved
loadout_selected = pyqtSignal(object) # Emitted when loadout is selected for use
def __init__(self, parent=None, config_dir: Optional[str] = None): def __init__(self, parent=None, config_dir: Optional[str] = None):
super().__init__(parent) super().__init__(parent)
@ -1086,6 +1087,9 @@ class LoadoutManagerDialog(QDialog):
# Buttons # Buttons
self.save_btn = QPushButton("💾 Save Loadout") self.save_btn = QPushButton("💾 Save Loadout")
self.save_btn.setObjectName("saveButton") self.save_btn.setObjectName("saveButton")
self.use_btn = QPushButton("✅ Use Loadout")
self.use_btn.setObjectName("useButton")
self.use_btn.setToolTip("Use this loadout for current session")
self.load_btn = QPushButton("📂 Load Selected") self.load_btn = QPushButton("📂 Load Selected")
self.delete_btn = QPushButton("🗑️ Delete") self.delete_btn = QPushButton("🗑️ Delete")
self.delete_btn.setObjectName("deleteButton") self.delete_btn.setObjectName("deleteButton")
@ -1112,6 +1116,7 @@ class LoadoutManagerDialog(QDialog):
left_btn_layout = QHBoxLayout() left_btn_layout = QHBoxLayout()
left_btn_layout.addWidget(self.load_btn) left_btn_layout.addWidget(self.load_btn)
left_btn_layout.addWidget(self.use_btn)
left_btn_layout.addWidget(self.delete_btn) left_btn_layout.addWidget(self.delete_btn)
left_layout.addLayout(left_btn_layout) left_layout.addLayout(left_btn_layout)
@ -1296,6 +1301,7 @@ class LoadoutManagerDialog(QDialog):
# Buttons # Buttons
self.save_btn.clicked.connect(self._save_loadout) self.save_btn.clicked.connect(self._save_loadout)
self.use_btn.clicked.connect(self._use_selected)
self.load_btn.clicked.connect(self._load_selected) self.load_btn.clicked.connect(self._load_selected)
self.delete_btn.clicked.connect(self._delete_selected) self.delete_btn.clicked.connect(self._delete_selected)
self.new_btn.clicked.connect(self._new_loadout) self.new_btn.clicked.connect(self._new_loadout)
@ -1686,6 +1692,37 @@ class LoadoutManagerDialog(QDialog):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}") QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
def _use_selected(self):
"""Use the selected loadout for the current session."""
item = self.saved_list.currentItem()
if not item:
QMessageBox.information(self, "No Selection", "Please select a loadout to use")
return
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.current_loadout = config
# Emit signal with the loadout for main window to use
self.loadout_selected.emit(config)
QMessageBox.information(self, "Loadout Selected",
f"Loadout '{config.name}' is now active for your session.\n\n"
f"Weapon: {config.weapon_name}\n"
f"Healing Tool: {config.heal_name}\n"
f"Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
self.accept() # Close dialog
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load loadout: {str(e)}")
def _delete_selected(self): def _delete_selected(self):
"""Delete the selected loadout.""" """Delete the selected loadout."""
item = self.saved_list.currentItem() item = self.saved_list.currentItem()

View File

@ -562,6 +562,11 @@ class MainWindow(QMainWindow):
select_finder_action.triggered.connect(lambda: self.on_select_gear("finder")) select_finder_action.triggered.connect(lambda: self.on_select_gear("finder"))
select_gear_menu.addAction(select_finder_action) select_gear_menu.addAction(select_finder_action)
select_medical_action = QAction("&Medical Tool", self)
select_medical_action.setShortcut("Ctrl+M")
select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool"))
select_gear_menu.addAction(select_medical_action)
tools_menu.addSeparator() tools_menu.addSeparator()
loadout_action = QAction("&Loadout Manager", self) loadout_action = QAction("&Loadout Manager", self)
@ -1345,8 +1350,25 @@ class MainWindow(QMainWindow):
"""Open Loadout Manager dialog.""" """Open Loadout Manager dialog."""
from ui.loadout_manager import LoadoutManagerDialog from ui.loadout_manager import LoadoutManagerDialog
dialog = LoadoutManagerDialog(self) dialog = LoadoutManagerDialog(self)
dialog.loadout_selected.connect(self.on_loadout_selected)
dialog.exec() dialog.exec()
def on_loadout_selected(self, loadout):
"""Handle loadout selection from Loadout Manager."""
self._selected_loadout = loadout
self.log_info("Loadout", f"Selected loadout: {loadout.name}")
# Update selected gear from loadout
if hasattr(loadout, 'weapon_name'):
self._selected_weapon = loadout.weapon_name
if hasattr(loadout, 'heal_cost_pec'):
# Create medical tool stats from loadout heal cost
self._selected_medical_tool = loadout.heal_name
self._selected_medical_tool_stats = {
'decay': float(loadout.heal_cost_pec),
'cost_per_heal': float(loadout.heal_cost_pec) / 100.0, # Convert PEC to PED
}
def on_select_gear(self, gear_type: str = "weapon"): def on_select_gear(self, gear_type: str = "weapon"):
"""Open Gear Selector dialog.""" """Open Gear Selector dialog."""
from ui.gear_selector import GearSelectorDialog from ui.gear_selector import GearSelectorDialog
@ -1370,6 +1392,11 @@ class MainWindow(QMainWindow):
elif gear_type == "finder": elif gear_type == "finder":
self._selected_finder = name self._selected_finder = name
self._selected_finder_stats = stats self._selected_finder_stats = stats
elif gear_type == "medical_tool":
self._selected_medical_tool = name
self._selected_medical_tool_stats = stats
if self.session_state == SessionState.RUNNING:
self.hud.update_stats({'medical_tool': name})
def on_about(self): def on_about(self):
"""Show about dialog.""" """Show about dialog."""