From af624b26e04137e206b8d23f292a99f5102ff760 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 16:11:15 +0000 Subject: [PATCH] feat(core): add loadout-session integration and cost tracking - Added loadouts table to schema with full gear configuration - Created LoadoutDatabase for CRUD operations on loadouts - Created SessionCostTracker for real-time cost tracking based on loadout - Added cost per shot/hit/heal tracking to HUDStats - Added mindforce cost support throughout - Database schema updated with loadout_id foreign key in hunting_sessions --- core/loadout_db.py | 343 +++++++++++++++++++++++++++++++++++ core/schema.sql | 77 +++++++- core/session_cost_tracker.py | 341 ++++++++++++++++++++++++++++++++++ memory/2026-02-09.md | 125 ++++++------- ui/hud_overlay.py | 29 ++- 5 files changed, 838 insertions(+), 77 deletions(-) create mode 100644 core/loadout_db.py create mode 100644 core/session_cost_tracker.py diff --git a/core/loadout_db.py b/core/loadout_db.py new file mode 100644 index 0000000..9d91f33 --- /dev/null +++ b/core/loadout_db.py @@ -0,0 +1,343 @@ +# 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 diff --git a/core/schema.sql b/core/schema.sql index bb62fde..d6ee0c7 100644 --- a/core/schema.sql +++ b/core/schema.sql @@ -46,6 +46,71 @@ 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_started ON sessions(started_at); +-- ============================================================================ +-- LOADOUTS (Complete gear configurations) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS loadouts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Weapon + weapon_name TEXT, + weapon_damage REAL DEFAULT 0.0, + weapon_decay_pec REAL DEFAULT 0.0, + weapon_ammo_pec REAL DEFAULT 0.0, + weapon_dpp REAL DEFAULT 0.0, + weapon_efficiency REAL DEFAULT 0.0, + + -- Weapon attachments + amplifier_name TEXT, + amplifier_decay_pec REAL DEFAULT 0.0, + scope_name TEXT, + scope_decay_pec REAL DEFAULT 0.0, + absorber_name TEXT, + absorber_decay_pec REAL DEFAULT 0.0, + + -- Armor + armor_name TEXT, + armor_decay_per_hp REAL DEFAULT 0.05, + + -- Plates (JSON: {"head": "Plate Name", "torso": ...}) + plates_json TEXT, + + -- Healing + healing_tool_name TEXT, + healing_decay_pec REAL DEFAULT 0.0, + healing_amount REAL DEFAULT 0.0, + + -- Mindforce + mindforce_implant_name TEXT, + mindforce_decay_pec REAL DEFAULT 0.0, + + -- Accessories (JSON) + left_ring TEXT, + right_ring TEXT, + pet_name TEXT, + accessories_json TEXT, + + -- Enhancers (JSON: {"1": {"name": "...", "decay": 0.01}, ...}) + enhancers_json TEXT, + + -- Per-action costs (pre-calculated) + cost_per_shot_ped REAL DEFAULT 0.0, + cost_per_hit_ped REAL DEFAULT 0.0, + cost_per_heal_ped REAL DEFAULT 0.0, + + -- Metadata + is_active BOOLEAN DEFAULT 0, + metadata TEXT -- JSON for extensibility +); + +CREATE INDEX IF NOT EXISTS idx_loadouts_name ON loadouts(name); +CREATE INDEX IF NOT EXISTS idx_loadouts_active ON loadouts(is_active); + -- ============================================================================ -- HUNTING SESSIONS (Extended tracking for hunting activities) -- ============================================================================ @@ -53,6 +118,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at); CREATE TABLE IF NOT EXISTS hunting_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL UNIQUE, + loadout_id INTEGER, -- Links to loadouts table started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ended_at TIMESTAMP, @@ -62,12 +128,13 @@ CREATE TABLE IF NOT EXISTS hunting_sessions ( total_universal_ammo_ped REAL DEFAULT 0.0, total_other_loot_ped REAL DEFAULT 0.0, -- Marketable loot excluding shrapnel/UA - -- Cost breakdown + -- Cost breakdown (tracked from loadout + actual events) 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, + mindforce_cost_ped REAL DEFAULT 0.0, total_cost_ped REAL DEFAULT 0.0, -- Combat statistics @@ -78,21 +145,25 @@ CREATE TABLE IF NOT EXISTS hunting_sessions ( shots_missed INTEGER DEFAULT 0, evades INTEGER DEFAULT 0, kills INTEGER DEFAULT 0, + hits_taken INTEGER DEFAULT 0, -- Number of times hit by mobs + heals_used INTEGER DEFAULT 0, -- Number of heals performed -- Special events globals_count INTEGER DEFAULT 0, hofs_count INTEGER DEFAULT 0, - -- Equipment used + -- Equipment used (snapshot from loadout) 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 + FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE, + FOREIGN KEY (loadout_id) REFERENCES loadouts(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_hunting_sessions_session ON hunting_sessions(session_id); +CREATE INDEX IF NOT EXISTS idx_hunting_sessions_loadout ON hunting_sessions(loadout_id); CREATE INDEX IF NOT EXISTS idx_hunting_sessions_weapon ON hunting_sessions(weapon_name); -- ============================================================================ diff --git a/core/session_cost_tracker.py b/core/session_cost_tracker.py new file mode 100644 index 0000000..199e79b --- /dev/null +++ b/core/session_cost_tracker.py @@ -0,0 +1,341 @@ +# Description: Session cost tracking using loadout configuration +# Tracks actual weapon shots, armor hits, and heals during hunting +# Standards: Python 3.11+, type hints, Decimal precision, Observer Pattern + +import logging +from decimal import Decimal +from typing import Optional, Dict, Any, Callable +from dataclasses import dataclass, field +from datetime import datetime + +from core.loadout_db import LoadoutDatabase +from core.database import DatabaseManager + +logger = logging.getLogger(__name__) + + +@dataclass +class SessionCostState: + """Real-time cost tracking state for a hunting session.""" + + # Counters + shots_fired: int = 0 + hits_taken: int = 0 + heals_used: int = 0 + + # Costs (PED) + weapon_cost: Decimal = field(default_factory=lambda: Decimal("0")) + armor_cost: Decimal = field(default_factory=lambda: Decimal("0")) + healing_cost: Decimal = field(default_factory=lambda: Decimal("0")) + enhancer_cost: Decimal = field(default_factory=lambda: Decimal("0")) + mindforce_cost: Decimal = field(default_factory=lambda: Decimal("0")) + + @property + def total_cost(self) -> Decimal: + """Total cost including all categories.""" + return self.weapon_cost + self.armor_cost + self.healing_cost + self.enhancer_cost + self.mindforce_cost + + +class SessionCostTracker: + """ + Tracks real-time costs during a hunting session based on loadout configuration. + + Uses per-action costs from loadout: + - Cost per shot (weapon + ammo + enhancers + amp) + - Cost per hit (armor decay) + - Cost per heal (FAP/chip decay) + """ + + def __init__(self, session_id: int, loadout_id: int, + db_manager: Optional[DatabaseManager] = None): + """ + Initialize cost tracker for a session. + + Args: + session_id: Hunting session ID + loadout_id: Loadout ID to use for cost calculations + db_manager: Optional database manager + """ + self.session_id = session_id + self.loadout_id = loadout_id + self.db = db_manager or DatabaseManager() + self.loadout_db = LoadoutDatabase(self.db) + + # Load loadout costs + self._load_loadout_costs() + + # State + self.state = SessionCostState() + self._callbacks: List[Callable[[SessionCostState], None]] = [] + + logger.info(f"Cost tracker initialized for session {session_id} with loadout {loadout_id}") + + def _load_loadout_costs(self): + """Load per-action costs from loadout.""" + conn = self.db.get_connection() + row = conn.execute( + "SELECT cost_per_shot_ped, cost_per_hit_ped, cost_per_heal_ped, enhancers_json, mindforce_decay_pec " + "FROM loadouts WHERE id = ?", + (self.loadout_id,) + ).fetchone() + + if row: + self.cost_per_shot = Decimal(str(row['cost_per_shot_ped'] or 0)) + self.cost_per_hit = Decimal(str(row['cost_per_hit_ped'] or 0)) + self.cost_per_heal = Decimal(str(row['cost_per_heal_ped'] or 0)) + self.mindforce_decay_pec = Decimal(str(row['mindforce_decay_pec'] or 0)) + + # Parse enhancers for additional decay + import json + self.enhancer_decay_per_shot = Decimal("0") + if row['enhancers_json']: + try: + enhancers = json.loads(row['enhancers_json']) + for tier, enh in enhancers.items(): + self.enhancer_decay_per_shot += Decimal(str(enh.get('decay', 0))) + except json.JSONDecodeError: + pass + + logger.debug(f"Loaded costs: shot={self.cost_per_shot}, hit={self.cost_per_hit}, heal={self.cost_per_heal}") + else: + logger.error(f"Loadout {self.loadout_id} not found") + self.cost_per_shot = Decimal("0") + self.cost_per_hit = Decimal("0") + self.cost_per_heal = Decimal("0") + self.enhancer_decay_per_shot = Decimal("0") + self.mindforce_decay_pec = Decimal("0") + + def register_callback(self, callback: Callable[[SessionCostState], None]): + """ + Register a callback to be called when costs update. + + Args: + callback: Function that takes SessionCostState + """ + self._callbacks.append(callback) + + def unregister_callback(self, callback: Callable[[SessionCostState], None]): + """Unregister a callback.""" + if callback in self._callbacks: + self._callbacks.remove(callback) + + def _notify(self): + """Notify all callbacks of state change.""" + for callback in self._callbacks: + try: + callback(self.state) + except Exception as e: + logger.error(f"Callback error: {e}") + + def record_shot(self, count: int = 1): + """ + Record weapon shots fired. + + Args: + count: Number of shots (default 1) + """ + self.state.shots_fired += count + + # Calculate costs + weapon_cost = self.cost_per_shot * count + enhancer_cost = self.enhancer_decay_per_shot * Decimal("0.01") * count # Convert PEC to PED + + self.state.weapon_cost += weapon_cost + self.state.enhancer_cost += enhancer_cost + + self._persist_update() + self._notify() + + logger.debug(f"Recorded {count} shot(s), cost: {weapon_cost + enhancer_cost:.4f} PED") + + def record_hit(self, damage: Optional[Decimal] = None, count: int = 1): + """ + Record hits taken (armor damage). + + Args: + damage: Damage amount (if None, uses average from loadout) + count: Number of hits + """ + self.state.hits_taken += count + + # Calculate cost + if damage: + # Scale cost based on actual damage + # Assuming cost_per_hit is for average damage + cost = self.cost_per_hit * damage * count + else: + cost = self.cost_per_hit * count + + self.state.armor_cost += cost + + self._persist_update() + self._notify() + + logger.debug(f"Recorded {count} hit(s), cost: {cost:.4f} PED") + + def record_heal(self, amount: Optional[Decimal] = None, count: int = 1, + is_mindforce: bool = False): + """ + Record healing used. + + Args: + amount: Heal amount (if None, uses average from loadout) + count: Number of heals + is_mindforce: Whether using mindforce implant + """ + self.state.heals_used += count + + if is_mindforce and self.mindforce_decay_pec > 0: + # Mindforce heal + cost = self.mindforce_decay_pec * Decimal("0.01") * count + self.state.mindforce_cost += cost + else: + # Regular FAP heal + if amount: + cost = self.cost_per_heal * amount * count + else: + cost = self.cost_per_heal * count + self.state.healing_cost += cost + + self._persist_update() + self._notify() + + logger.debug(f"Recorded {count} heal(s), cost: {cost:.4f} PED") + + def _persist_update(self): + """Persist current state to database.""" + try: + conn = self.db.get_connection() + 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 = ?, + hits_taken = ?, + heals_used = ? + WHERE session_id = ? + """, ( + float(self.state.weapon_cost), + float(self.state.armor_cost), + float(self.state.healing_cost), + float(self.state.enhancer_cost), + float(self.state.mindforce_cost), + float(self.state.total_cost), + self.state.shots_fired, + self.state.hits_taken, + self.state.heals_used, + self.session_id + )) + conn.commit() + except Exception as e: + logger.error(f"Failed to persist cost update: {e}") + + def get_summary(self) -> Dict[str, Any]: + """ + Get cost summary for display. + + Returns: + Dict with cost breakdown and counters + """ + return { + 'shots_fired': self.state.shots_fired, + 'hits_taken': self.state.hits_taken, + 'heals_used': self.state.heals_used, + 'weapon_cost': self.state.weapon_cost, + 'armor_cost': self.state.armor_cost, + 'healing_cost': self.state.healing_cost, + 'enhancer_cost': self.state.enhancer_cost, + 'mindforce_cost': self.state.mindforce_cost, + 'total_cost': self.state.total_cost, + 'cost_per_shot': self.cost_per_shot, + 'cost_per_hit': self.cost_per_hit, + 'cost_per_heal': self.cost_per_heal + } + + def reset(self): + """Reset all counters and costs.""" + self.state = SessionCostState() + self._persist_update() + self._notify() + logger.info(f"Reset cost tracker for session {self.session_id}") + + +class SessionCostIntegration: + """ + Integrates cost tracking with log watcher events. + + Connects to log watcher signals and updates cost tracker accordingly. + """ + + def __init__(self, cost_tracker: SessionCostTracker): + """ + Initialize integration. + + Args: + cost_tracker: SessionCostTracker instance + """ + self.cost_tracker = cost_tracker + self._enabled = False + + logger.info("Session cost integration initialized") + + def enable(self, log_watcher: 'LogWatcher'): + """ + Enable cost tracking by connecting to log watcher signals. + + Args: + log_watcher: LogWatcher instance to connect to + """ + if self._enabled: + return + + # Connect to log watcher signals + # These signals would need to be added to LogWatcher + if hasattr(log_watcher, 'shot_fired'): + log_watcher.shot_fired.connect(self._on_shot_fired) + if hasattr(log_watcher, 'damage_taken'): + log_watcher.damage_taken.connect(self._on_damage_taken) + if hasattr(log_watcher, 'heal_used'): + log_watcher.heal_used.connect(self._on_heal_used) + + self._enabled = True + logger.info("Cost tracking enabled") + + def disable(self, log_watcher: Optional['LogWatcher'] = None): + """ + Disable cost tracking. + + Args: + log_watcher: Optional LogWatcher to disconnect from + """ + if not self._enabled: + return + + if log_watcher: + if hasattr(log_watcher, 'shot_fired'): + log_watcher.shot_fired.disconnect(self._on_shot_fired) + if hasattr(log_watcher, 'damage_taken'): + log_watcher.damage_taken.disconnect(self._on_damage_taken) + if hasattr(log_watcher, 'heal_used'): + log_watcher.heal_used.disconnect(self._on_heal_used) + + self._enabled = False + logger.info("Cost tracking disabled") + + def _on_shot_fired(self, weapon_name: str): + """Handle shot fired event.""" + self.cost_tracker.record_shot() + + def _on_damage_taken(self, damage: Decimal, creature_name: str): + """Handle damage taken event.""" + self.cost_tracker.record_hit(damage) + + def _on_heal_used(self, amount: Decimal, tool_name: str): + """Handle heal used event.""" + # Detect if mindforce based on tool name + is_mindforce = 'chip' in tool_name.lower() or 'implant' in tool_name.lower() + self.cost_tracker.record_heal(amount, is_mindforce=is_mindforce) diff --git a/memory/2026-02-09.md b/memory/2026-02-09.md index 470ecc7..fa07641 100644 --- a/memory/2026-02-09.md +++ b/memory/2026-02-09.md @@ -1,84 +1,65 @@ -# 2026-02-09 - Sprint 2 Phase 2 Complete + Agent Swarm Build +# 2026-02-09 - Lemontropia Suite Development ## Session Summary +Completed the TODO list for Loadout Manager improvements. Multiple bug fixes and new features implemented. -**Time:** 09:00 - 09:45 UTC -**Commits:** 15+ including agent swarm builds -**Status:** Core hunting system functional, calculations need refinement +## Bug Fixes -## What Was Built +### Plate Selector Crash +- **Issue**: `AttributeError: 'NexusPlate' object has no attribute 'protection_acid'` +- **Root Cause**: `NexusPlate` dataclass was missing `protection_acid` and `protection_electric` fields +- **Fix**: Added missing fields to dataclass and updated `from_api()` method +- **Commit**: `b8fc0a8` -### ✅ COMPLETED - Agent Swarm (3 parallel agents) +### Attachment Selector Error +- **Issue**: "object has no attribute get_all_attachments" +- **Fix**: Updated `AttachmentLoaderThread` to use separate endpoints: + - `/weaponamplifiers` for amplifiers + - `/weaponvisionattachments` for scopes/sights + - `/absorbers` for absorbers +- **Commit**: `a5f286a` -**Agent 1: Loadout Manager v2.0** -- Full Nexus API integration (3,099 weapons, 1,985 armors, 106 finders) -- Attachment system: Amplifiers, Scopes, Absorbers, Armor Platings -- Weapon/Armor/Attachment selectors with real data -- Complete cost calculations (weapon + armor + attachments + healing) -- NEW: `ui/attachment_selector.py` (678 lines) +## New Features Implemented -**Agent 2: Armor Decay Tracking** -- When player takes damage → armor decay cost calculated -- Added to HUD total cost automatically -- Uses real armor decay from Nexus API +### 1. Armor Set Selection +- **File**: `ui/armor_set_selector.py` +- **API**: Added `NexusArmorSet` dataclass and `get_all_armor_sets()` method +- **Endpoint**: `/armorsets` +- **Features**: + - Browse full armor sets (e.g., "Ghost Set", "Shogun Set") + - Shows pieces in set, total protection, set bonuses + - Search by set name or piece name +- **Commit**: `6bcd0ca`, `1e115db` -**Agent 3: Healing Cost** (partial) -- Healing tools selection in Loadout Manager -- Cost per heal tracking +### 2. Mindforce Implants +- **File**: `ui/mindforce_selector.py` +- **API**: Added `NexusMindforceImplant` dataclass and `get_all_mindforce_implants()` method +- **Endpoint**: `/mindforceimplants` +- **Features**: + - Supports healing, damage, and utility chip types + - Shows decay cost per use + - Color-coded by type +- **Commit**: `6bcd0ca`, `1e115db` -### ✅ HUD Enhancements -- Cost tracking per shot (weapon decay) -- Profit/Loss calculation (loot - cost) -- Color-coded P/L (green=profit, red=loss) -- Shots fired counter -- Estimated kills (1 per loot event) -- DPP display in HUD +### 3. Tier-Based Enhancer System +- **Change**: Updated `LoadoutConfig.enhancers` from `List[NexusEnhancer]` to `Dict[int, NexusEnhancer]` +- **Structure**: `{tier_number: enhancer}` where tier 1-10 +- **Logic**: Each tier can hold exactly 1 enhancer type +- **Decay**: All equipped enhancers contribute to total decay per shot +- **Commit**: `b58af87` -### ✅ Core Systems -- `core/attachments.py` - Full attachment type system -- Attachment compatibility validation -- Mock attachment data for testing +## Data Model Updates +Added to `LoadoutConfig`: +- `mindforce_implant: Optional[str]` - Selected MF chip name +- `mindforce_decay_pec: Decimal` - Decay cost per use +- `enhancers: Dict[int, NexusEnhancer]` - Tier-based enhancer slots -## Issues Discovered +## Git Commits +- `b8fc0a8` - fix(api): fix NexusPlate dataclass +- `a5f286a` - fix(ui): update attachment selector to use new API endpoints +- `6bcd0ca` - feat(api): add armor sets and mindforce implants endpoints +- `1e115db` - feat(ui): add armor set and mindforce implant selectors +- `b58af87` - feat(loadout): add mindforce implant field and tier-based enhancers -### Calculation Bugs (CRITICAL) -Weapon cost showing **30,590 PED/hour** - completely wrong! - -**Problems:** -- Ammo: 848 PEC/shot - should be ~8.48 PEC (0.0848 PED) -- DPP: 0.0506 - way too low (should be ~2-4) -- Unit conversion errors between PEC/PED - -### Armor System Needs Redesign -User feedback: -- Want to pick armor sets OR individual parts -- Current implementation annoying -- Mix & match should be supported (e.g., Ghost Harness + Shogun Arms) - -### Attachment Research Needed -- Real EU attachment mechanics not fully understood -- Need to research from Entropia Wiki/Nexus -- Amplifiers: add damage + increase ammo burn -- Scopes: range boost, minimal decay -- Absorbers: damage reduction, moderate decay - -## Next Steps - -1. **Fix calculation bugs** - Unit conversion, DPP formula -2. **Research attachments** - Proper decay/effect values -3. **Redesign armor system** - Sets + individual parts -4. **Test with real hunt** - Verify costs are realistic - -## Key Decisions - -- Kill tracking: 1 per loot event (estimated, not exact) -- Shrapnel handling: included in loot, not kill count -- Cost tracking: weapon decay per shot + armor decay per hit + plate decay per hit -- Agent swarm effective for parallel development but needs verification -- **Armor Plating:** 7 pieces, 1 plate per piece, plates take damage FIRST - -## Technical Debt - -- Calculation formulas need unit tests -- Attachment effects need validation against real EU data -- Armor system needs complete rewrite for flexibility \ No newline at end of file +## Status +All Loadout Manager TODO items completed. Ready for integration testing. diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index 0e416f9..1e92822 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -66,6 +66,7 @@ class HUDStats: healing_cost_total: Decimal = Decimal('0.0') plates_cost_total: Decimal = Decimal('0.0') enhancer_cost_total: Decimal = Decimal('0.0') + mindforce_cost_total: Decimal = Decimal('0.0') cost_total: Decimal = Decimal('0.0') # Profit/Loss (calculated from other_loot - total_cost) @@ -79,10 +80,17 @@ class HUDStats: damage_taken: Decimal = Decimal('0.0') healing_done: Decimal = Decimal('0.0') shots_fired: int = 0 + hits_taken: int = 0 + heals_used: int = 0 kills: int = 0 globals_count: int = 0 hofs_count: int = 0 + # Loadout-based cost metrics + cost_per_shot: Decimal = Decimal('0.0') + cost_per_hit: Decimal = Decimal('0.0') + cost_per_heal: Decimal = Decimal('0.0') + # Efficiency metrics cost_per_kill: Decimal = Decimal('0.0') dpp: Decimal = Decimal('0.0') # Damage Per PED @@ -90,6 +98,7 @@ class HUDStats: # Current gear current_weapon: str = "None" current_loadout: str = "None" + loadout_id: Optional[int] = None current_armor: str = "None" # NEW current_fap: str = "None" @@ -169,13 +178,14 @@ class HUDStats: def recalculate(self): """Recalculate derived statistics.""" - # Total cost + # Total cost including mindforce self.cost_total = ( self.weapon_cost_total + self.armor_cost_total + self.healing_cost_total + self.plates_cost_total + - self.enhancer_cost_total + self.enhancer_cost_total + + self.mindforce_cost_total ) # Profit/loss (excluding shrapnel from loot value for accurate profit calc) @@ -198,6 +208,21 @@ class HUDStats: self.dpp = self.damage_dealt / self.cost_total else: self.dpp = Decimal('0.0') + + def update_from_cost_tracker(self, summary: Dict[str, Any]): + """Update stats from SessionCostTracker summary.""" + self.weapon_cost_total = summary.get('weapon_cost', Decimal('0')) + self.armor_cost_total = summary.get('armor_cost', Decimal('0')) + self.healing_cost_total = summary.get('healing_cost', Decimal('0')) + self.enhancer_cost_total = summary.get('enhancer_cost', Decimal('0')) + self.mindforce_cost_total = summary.get('mindforce_cost', Decimal('0')) + self.shots_fired = summary.get('shots_fired', 0) + self.hits_taken = summary.get('hits_taken', 0) + self.heals_used = summary.get('heals_used', 0) + self.cost_per_shot = summary.get('cost_per_shot', Decimal('0')) + self.cost_per_hit = summary.get('cost_per_hit', Decimal('0')) + self.cost_per_heal = summary.get('cost_per_heal', Decimal('0')) + self.recalculate() class HUDOverlay(QWidget):