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
This commit is contained in:
LemonNexus 2026-02-09 16:11:15 +00:00
parent b58af87533
commit af624b26e0
5 changed files with 838 additions and 77 deletions

343
core/loadout_db.py Normal file
View File

@ -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

View File

@ -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);
-- ============================================================================

View File

@ -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)

View File

@ -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
## Status
All Loadout Manager TODO items completed. Ready for integration testing.

View File

@ -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):