feat(hunting): complete hunting session tracker

- HuntingSession dataclass with full cost/loot tracking
- Real-time cost calculations (weapon + armor + healing)
- Profit/Loss with return percentage
- Cost per kill and cost per hour metrics
- SessionManager for multiple sessions
- JSON export for session data
- Excludes shrapnel/UA from profit calculations
This commit is contained in:
LemonNexus 2026-02-09 10:38:37 +00:00
parent b8d5b4a50e
commit 17bb5cbf0b
2 changed files with 403 additions and 7 deletions

View File

@ -781,13 +781,16 @@ def create_ghost_set() -> ArmorSet:
def create_shogun_set() -> ArmorSet: def create_shogun_set() -> ArmorSet:
"""Create the Shogun armor set (medium, good vs impact/cut).""" """Create the Shogun armor set (medium, good vs impact/cut)."""
# Shogun uses standard 20 hp/pec economy
shogun_economy = Decimal("0.05")
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Shogun Helmet", name="Shogun Helmet",
item_id="shogun_helmet", item_id="shogun_helmet",
slot=ArmorSlot.HEAD, slot=ArmorSlot.HEAD,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.025"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")), protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
weight=Decimal("0.8"), weight=Decimal("0.8"),
), ),
@ -796,7 +799,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_harness", item_id="shogun_harness",
slot=ArmorSlot.CHEST, slot=ArmorSlot.CHEST,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.060"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("5"), burn=Decimal("4"), cold=Decimal("4")), protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("5"), burn=Decimal("4"), cold=Decimal("4")),
weight=Decimal("1.5"), weight=Decimal("1.5"),
), ),
@ -805,7 +808,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_arm_l", item_id="shogun_arm_l",
slot=ArmorSlot.LEFT_ARM, slot=ArmorSlot.LEFT_ARM,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.025"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")), protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
weight=Decimal("0.8"), weight=Decimal("0.8"),
), ),
@ -814,7 +817,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_arm_r", item_id="shogun_arm_r",
slot=ArmorSlot.RIGHT_ARM, slot=ArmorSlot.RIGHT_ARM,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.025"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")), protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
weight=Decimal("0.8"), weight=Decimal("0.8"),
), ),
@ -823,7 +826,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_gloves_l", item_id="shogun_gloves_l",
slot=ArmorSlot.LEFT_HAND, slot=ArmorSlot.LEFT_HAND,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.015"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")), protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")),
weight=Decimal("0.4"), weight=Decimal("0.4"),
), ),
@ -832,7 +835,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_gloves_r", item_id="shogun_gloves_r",
slot=ArmorSlot.RIGHT_HAND, slot=ArmorSlot.RIGHT_HAND,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.015"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")), protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")),
weight=Decimal("0.4"), weight=Decimal("0.4"),
), ),
@ -841,7 +844,7 @@ def create_shogun_set() -> ArmorSet:
item_id="shogun_legs", item_id="shogun_legs",
slot=ArmorSlot.LEGS, slot=ArmorSlot.LEGS,
set_name="Shogun", set_name="Shogun",
decay_per_hit=Decimal("0.050"), decay_per_hp=shogun_economy,
protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("5"), stab=Decimal("4"), burn=Decimal("3"), cold=Decimal("3")), protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("5"), stab=Decimal("4"), burn=Decimal("3"), cold=Decimal("3")),
weight=Decimal("1.2"), weight=Decimal("1.2"),
), ),

393
core/hunting_session.py Normal file
View File

@ -0,0 +1,393 @@
"""
Hunting Session Tracker for Lemontropia Suite
Complete session tracking with cost, loot, and profit/loss calculations.
"""
from decimal import Decimal
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
import json
import logging
import sys
from pathlib import Path
# Add parent to path for standalone testing
if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent.parent))
try:
from core.healing_tools import HealingTool, get_healing_tool
from core.armor_decay import calculate_armor_decay, get_armor_durability
except ImportError:
# Fallback for standalone testing
def calculate_armor_decay(damage, durability):
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
decay_pec = damage * Decimal("0.05") * durability_factor
return decay_pec / Decimal(100)
logger = logging.getLogger(__name__)
@dataclass
class HuntingSession:
"""Complete hunting session tracking."""
# Session identification
session_id: str
project_id: int
start_time: datetime
end_time: Optional[datetime] = None
# Gear configuration
weapon_name: str = "Unknown"
weapon_damage: Decimal = Decimal('0')
weapon_decay_pec: Decimal = Decimal('0')
weapon_ammo_burn: Decimal = Decimal('0')
armor_name: str = "Unknown"
armor_durability: int = 2000
medical_tool_name: str = "None"
medical_tool_decay_pec: Decimal = Decimal('0')
# Combat statistics
shots_fired: int = 0
damage_dealt: Decimal = Decimal('0')
damage_taken: Decimal = Decimal('0')
kills: int = 0
# Healing statistics
heals_performed: int = 0
health_healed: Decimal = Decimal('0')
# Loot tracking
loot_events: List[Dict[str, Any]] = field(default_factory=list)
total_loot_value: Decimal = Decimal('0')
shrapnel_value: Decimal = Decimal('0')
universal_ammo_value: Decimal = Decimal('0')
other_loot_value: Decimal = Decimal('0')
# Special events
globals_count: int = 0
hofs_count: int = 0
personal_globals: List[Dict] = field(default_factory=list)
# Cost tracking (calculated)
_weapon_cost: Decimal = field(default=Decimal('0'), repr=False)
_armor_cost: Decimal = field(default=Decimal('0'), repr=False)
_healing_cost: Decimal = field(default=Decimal('0'), repr=False)
_plate_cost: Decimal = field(default=Decimal('0'), repr=False)
def add_shot(self):
"""Record a weapon shot."""
self.shots_fired += 1
self._recalculate_weapon_cost()
def add_damage_dealt(self, damage: Decimal):
"""Record damage dealt to creatures."""
self.damage_dealt += damage
def add_damage_taken(self, damage: Decimal):
"""Record damage taken from creatures."""
self.damage_taken += damage
self._recalculate_armor_cost()
def add_heal(self, amount: Decimal):
"""Record a healing action."""
self.heals_performed += 1
self.health_healed += amount
self._recalculate_healing_cost()
def add_kill(self):
"""Record a creature kill."""
self.kills += 1
def add_loot(self, item_name: str, quantity: int, value_ped: Decimal,
is_shrapnel: bool = False, is_universal_ammo: bool = False):
"""Record loot received."""
loot_event = {
'timestamp': datetime.now().isoformat(),
'item': item_name,
'quantity': quantity,
'value': str(value_ped),
'is_shrapnel': is_shrapnel,
'is_universal_ammo': is_universal_ammo
}
self.loot_events.append(loot_event)
if is_shrapnel:
self.shrapnel_value += value_ped
elif is_universal_ammo:
self.universal_ammo_value += value_ped
else:
self.other_loot_value += value_ped
self.total_loot_value += value_ped
def add_global(self, is_hof: bool = False, value_ped: Decimal = Decimal('0')):
"""Record a global or HOF."""
if is_hof:
self.hofs_count += 1
else:
self.globals_count += 1
self.personal_globals.append({
'timestamp': datetime.now().isoformat(),
'is_hof': is_hof,
'value': str(value_ped)
})
def _recalculate_weapon_cost(self):
"""Calculate weapon cost based on shots fired."""
# Cost per shot = (decay + ammo_cost) / 100 (convert to PED)
ammo_cost_pec = self.weapon_ammo_burn * Decimal('0.01') # 1 ammo = 0.01 PEC
cost_per_shot = (self.weapon_decay_pec + ammo_cost_pec) / Decimal('100')
self._weapon_cost = cost_per_shot * self.shots_fired
def _recalculate_armor_cost(self):
"""Calculate armor cost based on damage taken."""
# Using official decay formula
self._armor_cost = calculate_armor_decay(self.damage_taken, self.armor_durability)
def _recalculate_healing_cost(self):
"""Calculate healing cost based on heals performed."""
self._healing_cost = (self.medical_tool_decay_pec * self.heals_performed) / Decimal('100')
@property
def total_cost(self) -> Decimal:
"""Get total hunting cost (PED)."""
return self._weapon_cost + self._armor_cost + self._healing_cost + self._plate_cost
@property
def profit_loss(self) -> Decimal:
"""Calculate profit/loss (PED) - excludes shrapnel/UA."""
return self.total_loot_value - self.total_cost
@property
def return_percentage(self) -> Decimal:
"""Calculate return percentage (loot/cost * 100)."""
if self.total_cost > 0:
return (self.total_loot_value / self.total_cost) * Decimal('100')
return Decimal('0')
@property
def cost_per_kill(self) -> Decimal:
"""Calculate cost per kill."""
if self.kills > 0:
return self.total_cost / self.kills
return Decimal('0')
@property
def session_duration(self) -> timedelta:
"""Get session duration."""
end = self.end_time or datetime.now()
return end - self.start_time
@property
def cost_per_hour(self) -> Decimal:
"""Calculate cost per hour."""
hours = self.session_duration.total_seconds() / 3600
if hours > 0:
return self.total_cost / Decimal(str(hours))
return Decimal('0')
@property
def loot_per_hour(self) -> Decimal:
"""Calculate loot value per hour."""
hours = self.session_duration.total_seconds() / 3600
if hours > 0:
return self.total_loot_value / Decimal(str(hours))
return Decimal('0')
def get_summary(self) -> Dict[str, Any]:
"""Get session summary as dictionary."""
return {
'session_id': self.session_id,
'duration_minutes': self.session_duration.total_seconds() / 60,
'weapon': self.weapon_name,
'armor': self.armor_name,
'medical_tool': self.medical_tool_name,
'combat': {
'shots_fired': self.shots_fired,
'damage_dealt': str(self.damage_dealt),
'damage_taken': str(self.damage_taken),
'kills': self.kills,
},
'healing': {
'heals_performed': self.heals_performed,
'health_healed': str(self.health_healed),
},
'loot': {
'total_value': str(self.total_loot_value),
'shrapnel': str(self.shrapnel_value),
'universal_ammo': str(self.universal_ammo_value),
'other': str(self.other_loot_value),
'events_count': len(self.loot_events),
},
'special_events': {
'globals': self.globals_count,
'hofs': self.hofs_count,
},
'costs': {
'weapon': str(self._weapon_cost),
'armor': str(self._armor_cost),
'healing': str(self._healing_cost),
'plates': str(self._plate_cost),
'total': str(self.total_cost),
},
'results': {
'profit_loss': str(self.profit_loss),
'return_percentage': float(self.return_percentage),
'cost_per_kill': str(self.cost_per_kill),
'cost_per_hour': str(self.cost_per_hour),
'loot_per_hour': str(self.loot_per_hour),
}
}
def to_json(self) -> str:
"""Export session to JSON."""
return json.dumps(self.get_summary(), indent=2)
class SessionManager:
"""Manages multiple hunting sessions."""
def __init__(self):
self.active_session: Optional[HuntingSession] = None
self.completed_sessions: List[HuntingSession] = []
def start_session(self, session_id: str, project_id: int, **gear_config) -> HuntingSession:
"""Start a new hunting session."""
session = HuntingSession(
session_id=session_id,
project_id=project_id,
start_time=datetime.now(),
**gear_config
)
self.active_session = session
logger.info(f"Started hunting session {session_id}")
return session
def end_session(self) -> Optional[HuntingSession]:
"""End the current session."""
if self.active_session:
self.active_session.end_time = datetime.now()
self.completed_sessions.append(self.active_session)
session = self.active_session
self.active_session = None
logger.info(f"Ended hunting session {session.session_id}")
return session
return None
def get_active_session(self) -> Optional[HuntingSession]:
"""Get currently active session."""
return self.active_session
def get_session_history(self) -> List[HuntingSession]:
"""Get all completed sessions."""
return self.completed_sessions.copy()
def get_totals(self) -> Dict[str, Decimal]:
"""Get totals across all sessions."""
totals = {
'total_loot': Decimal('0'),
'total_cost': Decimal('0'),
'total_profit': Decimal('0'),
'total_kills': Decimal('0'),
}
for session in self.completed_sessions:
totals['total_loot'] += session.total_loot_value
totals['total_cost'] += session.total_cost
totals['total_profit'] += session.profit_loss
totals['total_kills'] += Decimal(session.kills)
return totals
# Global session manager instance
_session_manager = SessionManager()
def get_session_manager() -> SessionManager:
"""Get the global session manager."""
return _session_manager
if __name__ == "__main__":
# Test the session tracking
print("Testing Hunting Session Tracker")
print("=" * 60)
# Create a test session
session = HuntingSession(
session_id="test-001",
project_id=1,
start_time=datetime.now(),
weapon_name="ArMatrix BP-25",
weapon_damage=Decimal('39'),
weapon_decay_pec=Decimal('0.688'),
weapon_ammo_burn=Decimal('848'),
armor_name="Ghost",
armor_durability=2000,
medical_tool_name="Vivo S10",
medical_tool_decay_pec=Decimal('1.705')
)
# Simulate a hunting session
print("\nSimulating 10-minute hunt...")
# Fire 100 shots
for _ in range(100):
session.add_shot()
session.add_damage_dealt(Decimal('39'))
# Take some damage
session.add_damage_taken(Decimal('150'))
# Heal twice
session.add_heal(Decimal('21'))
session.add_heal(Decimal('21'))
# Kill 5 mobs
for _ in range(5):
session.add_kill()
# Get some loot
session.add_loot("Animal Hide", 5, Decimal('0.50'))
session.add_loot("Animal Oil", 3, Decimal('0.30'))
session.add_loot("Shrapnel", 100, Decimal('1.00'), is_shrapnel=True)
# End session
session.end_time = datetime.now() + timedelta(minutes=10)
# Print summary
print(f"\nSession Summary:")
print(f" Duration: {session.session_duration}")
print(f" Shots: {session.shots_fired}")
print(f" Damage Dealt: {session.damage_dealt}")
print(f" Damage Taken: {session.damage_taken}")
print(f" Kills: {session.kills}")
print(f" Heals: {session.heals_performed}")
print(f"\nCosts:")
print(f" Weapon: {session._weapon_cost:.4f} PED")
print(f" Armor: {session._armor_cost:.4f} PED")
print(f" Healing: {session._healing_cost:.4f} PED")
print(f" Total: {session.total_cost:.4f} PED")
print(f"\nLoot:")
print(f" Total: {session.total_loot_value:.4f} PED")
print(f" Shrapnel: {session.shrapnel_value:.4f} PED")
print(f"\nResults:")
print(f" Profit/Loss: {session.profit_loss:.4f} PED")
print(f" Return: {session.return_percentage:.2f}%")
print(f" Cost/Kill: {session.cost_per_kill:.4f} PED")
print(f" Cost/Hour: {session.cost_per_hour:.2f} PED")