# Description: Loadout database operations for Lemontropia Suite # Manages saving, loading, and retrieving complete gear configurations # Standards: Python 3.11+, type hints, Decimal precision for PED/PEC import json import logging from decimal import Decimal from typing import Optional, List, Dict, Any from dataclasses import asdict from core.database import DatabaseManager logger = logging.getLogger(__name__) class LoadoutDatabase: """Database operations for loadout management.""" def __init__(self, db_manager: Optional[DatabaseManager] = None): """Initialize with database manager.""" self.db = db_manager or DatabaseManager() def save_loadout(self, name: str, config: 'LoadoutConfig', description: str = "") -> int: """ Save a loadout configuration to database. Args: name: Unique loadout name config: LoadoutConfig object description: Optional description Returns: Loadout ID """ conn = self.db.get_connection() # Convert config to JSON-serializable dict plates_json = json.dumps(config.armor_plates) if config.armor_plates else "{}" enhancers_json = json.dumps( {str(k): {"name": v.name, "decay": float(v.decay)} for k, v in config.enhancers.items()} ) if config.enhancers else "{}" accessories_json = json.dumps({ "left_ring": config.left_ring, "right_ring": config.right_ring, "clothing": config.clothing_items, "pet": config.pet }) cursor = conn.execute(""" INSERT OR REPLACE INTO loadouts ( name, description, updated_at, weapon_name, weapon_damage, weapon_decay_pec, weapon_ammo_pec, weapon_dpp, weapon_efficiency, amplifier_name, amplifier_decay_pec, scope_name, scope_decay_pec, absorber_name, absorber_decay_pec, armor_name, armor_decay_per_hp, plates_json, healing_tool_name, healing_decay_pec, healing_amount, mindforce_implant_name, mindforce_decay_pec, left_ring, right_ring, pet_name, accessories_json, enhancers_json, cost_per_shot_ped, cost_per_hit_ped, cost_per_heal_ped ) VALUES ( ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) """, ( name, description, config.weapon_name, float(config.weapon_damage), float(config.weapon_decay_pec), float(config.weapon_ammo_pec), float(config.weapon_dpp), float(config.weapon_efficiency), config.weapon_amplifier.name if config.weapon_amplifier else None, float(config.weapon_amplifier.decay_pec) if config.weapon_amplifier else 0.0, config.weapon_scope.name if config.weapon_scope else None, float(config.weapon_scope.decay_pec) if config.weapon_scope else 0.0, config.weapon_absorber.name if config.weapon_absorber else None, float(config.weapon_absorber.decay_pec) if config.weapon_absorber else 0.0, config.armor_name, float(config.armor_decay_per_hp), plates_json, config.heal_name, float(config.heal_cost_pec), float(config.heal_amount), config.mindforce_implant, float(config.mindforce_decay_pec), config.left_ring, config.right_ring, config.pet, accessories_json, enhancers_json, float(config.get_cost_per_shot()), float(config.get_cost_per_hit()), float(config.get_cost_per_heal()) )) conn.commit() loadout_id = cursor.lastrowid logger.info(f"Saved loadout '{name}' (ID: {loadout_id})") return loadout_id def get_loadout(self, name: str) -> Optional[Dict[str, Any]]: """ Retrieve a loadout by name. Args: name: Loadout name Returns: Loadout data dict or None """ conn = self.db.get_connection() cursor = conn.execute( "SELECT * FROM loadouts WHERE name = ?", (name,) ) row = cursor.fetchone() if row: return dict(row) return None def list_loadouts(self) -> List[Dict[str, Any]]: """ List all saved loadouts. Returns: List of loadout dicts """ conn = self.db.get_connection() cursor = conn.execute( """SELECT id, name, description, weapon_name, armor_name, cost_per_shot_ped, cost_per_hit_ped, cost_per_heal_ped, is_active, created_at, updated_at FROM loadouts ORDER BY updated_at DESC""" ) return [dict(row) for row in cursor.fetchall()] def set_active_loadout(self, loadout_id: int) -> bool: """ Set a loadout as the active one. Args: loadout_id: Loadout ID to activate Returns: True if successful """ conn = self.db.get_connection() # Clear all active flags first conn.execute("UPDATE loadouts SET is_active = 0") # Set new active cursor = conn.execute( "UPDATE loadouts SET is_active = 1 WHERE id = ?", (loadout_id,) ) conn.commit() if cursor.rowcount > 0: logger.info(f"Set loadout {loadout_id} as active") return True return False def get_active_loadout(self) -> Optional[Dict[str, Any]]: """ Get the currently active loadout. Returns: Active loadout dict or None """ conn = self.db.get_connection() cursor = conn.execute( "SELECT * FROM loadouts WHERE is_active = 1 LIMIT 1" ) row = cursor.fetchone() if row: return dict(row) return None def delete_loadout(self, name: str) -> bool: """ Delete a loadout by name. Args: name: Loadout name to delete Returns: True if deleted """ conn = self.db.get_connection() cursor = conn.execute( "DELETE FROM loadouts WHERE name = ?", (name,) ) conn.commit() if cursor.rowcount > 0: logger.info(f"Deleted loadout '{name}'") return True return False def link_loadout_to_session(self, session_id: int, loadout_id: int) -> bool: """ Link a loadout to a hunting session. Args: session_id: Hunting session ID loadout_id: Loadout ID Returns: True if successful """ conn = self.db.get_connection() # Get loadout data loadout = conn.execute( "SELECT * FROM loadouts WHERE id = ?", (loadout_id,) ).fetchone() if not loadout: logger.error(f"Loadout {loadout_id} not found") return False # Update hunting session with loadout reference and snapshot conn.execute(""" UPDATE hunting_sessions SET loadout_id = ?, weapon_name = ?, armor_name = ?, fap_name = ? WHERE session_id = ? """, ( loadout_id, loadout['weapon_name'], loadout['armor_name'], loadout['healing_tool_name'], session_id )) conn.commit() logger.info(f"Linked loadout {loadout_id} to session {session_id}") return True def update_session_costs(self, session_id: int, shots: int = 0, hits: int = 0, heals: int = 0) -> Dict[str, Decimal]: """ Calculate and update costs for a hunting session based on loadout. Args: session_id: Hunting session ID shots: Number of shots fired hits: Number of hits taken heals: Number of heals used Returns: Dict with cost breakdown """ conn = self.db.get_connection() # Get session with linked loadout session = conn.execute(""" SELECT hs.*, l.* FROM hunting_sessions hs LEFT JOIN loadouts l ON hs.loadout_id = l.id WHERE hs.session_id = ? """, (session_id,)).fetchone() if not session: logger.error(f"Session {session_id} not found") return {} # Calculate costs weapon_cost = Decimal(str(session['cost_per_shot_ped'] or 0)) * shots armor_cost = Decimal(str(session['cost_per_hit_ped'] or 0)) * hits healing_cost = Decimal(str(session['cost_per_heal_ped'] or 0)) * heals # Add enhancer costs (if applicable) enhancer_cost = Decimal("0") if session['enhancers_json']: try: enhancers = json.loads(session['enhancers_json']) for tier, enh in enhancers.items(): enhancer_cost += Decimal(str(enh.get('decay', 0))) * shots except json.JSONDecodeError: pass # Add mindforce costs mindforce_cost = Decimal(str(session['mindforce_decay_pec'] or 0)) * heals * Decimal("0.01") total_cost = weapon_cost + armor_cost + healing_cost + enhancer_cost + mindforce_cost # Update session conn.execute(""" UPDATE hunting_sessions SET weapon_cost_ped = ?, armor_cost_ped = ?, healing_cost_ped = ?, enhancer_cost_ped = ?, mindforce_cost_ped = ?, total_cost_ped = ?, shots_fired = shots_fired + ?, hits_taken = hits_taken + ?, heals_used = heals_used + ? WHERE session_id = ? """, ( float(weapon_cost), float(armor_cost), float(healing_cost), float(enhancer_cost), float(mindforce_cost), float(total_cost), shots, hits, heals, session_id )) conn.commit() costs = { 'weapon': weapon_cost, 'armor': armor_cost, 'healing': healing_cost, 'enhancer': enhancer_cost, 'mindforce': mindforce_cost, 'total': total_cost } logger.debug(f"Updated session {session_id} costs: {costs}") return costs