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:
parent
b8d5b4a50e
commit
17bb5cbf0b
|
|
@ -781,13 +781,16 @@ def create_ghost_set() -> ArmorSet:
|
|||
|
||||
def create_shogun_set() -> ArmorSet:
|
||||
"""Create the Shogun armor set (medium, good vs impact/cut)."""
|
||||
# Shogun uses standard 20 hp/pec economy
|
||||
shogun_economy = Decimal("0.05")
|
||||
|
||||
pieces = {
|
||||
ArmorSlot.HEAD: ArmorPiece(
|
||||
name="Shogun Helmet",
|
||||
item_id="shogun_helmet",
|
||||
slot=ArmorSlot.HEAD,
|
||||
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")),
|
||||
weight=Decimal("0.8"),
|
||||
),
|
||||
|
|
@ -796,7 +799,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_harness",
|
||||
slot=ArmorSlot.CHEST,
|
||||
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")),
|
||||
weight=Decimal("1.5"),
|
||||
),
|
||||
|
|
@ -805,7 +808,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_arm_l",
|
||||
slot=ArmorSlot.LEFT_ARM,
|
||||
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")),
|
||||
weight=Decimal("0.8"),
|
||||
),
|
||||
|
|
@ -814,7 +817,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_arm_r",
|
||||
slot=ArmorSlot.RIGHT_ARM,
|
||||
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")),
|
||||
weight=Decimal("0.8"),
|
||||
),
|
||||
|
|
@ -823,7 +826,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_gloves_l",
|
||||
slot=ArmorSlot.LEFT_HAND,
|
||||
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")),
|
||||
weight=Decimal("0.4"),
|
||||
),
|
||||
|
|
@ -832,7 +835,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_gloves_r",
|
||||
slot=ArmorSlot.RIGHT_HAND,
|
||||
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")),
|
||||
weight=Decimal("0.4"),
|
||||
),
|
||||
|
|
@ -841,7 +844,7 @@ def create_shogun_set() -> ArmorSet:
|
|||
item_id="shogun_legs",
|
||||
slot=ArmorSlot.LEGS,
|
||||
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")),
|
||||
weight=Decimal("1.2"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
Loading…
Reference in New Issue