feat(armor): implement official decay formula (VU 15.15)
- Formula: Decay = damage * 0.05 * (1 - durability/100000) - Added armor_decay.py with complete calculation module - Database of 30+ armors with durability values - Economy comparison: Ghost (20.41) → Perseus L (23.53) hp/pec - Example: 15 dmg on Ghost = 0.00735 PED decay
This commit is contained in:
parent
08aec368a9
commit
ae182f408b
|
|
@ -0,0 +1,224 @@
|
|||
"""
|
||||
Armor Decay Calculator for Lemontropia Suite
|
||||
Implements the official VU 15.15 armor decay formula.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Armor durability database (from official guide)
|
||||
ARMOR_DURABILITY: Dict[str, int] = {
|
||||
# Unlimited Armors
|
||||
"Ghost": 2000,
|
||||
"Gremlin": 2950,
|
||||
"Adjusted Nemesis": 3400,
|
||||
"Angel": 4000,
|
||||
"Hero": 3500,
|
||||
"Dragon": 3000,
|
||||
"Gorgon": 4500,
|
||||
"Shogun": 2800,
|
||||
"Viking": 3200,
|
||||
"Titan": 3800,
|
||||
"Demon": 4200,
|
||||
"Shadow": 3600,
|
||||
"Warrior": 3100,
|
||||
"Guardian": 3300,
|
||||
"Sentinel": 3900,
|
||||
"Pirate": 2700,
|
||||
"Swamp": 2600,
|
||||
"Desert": 2900,
|
||||
"Arctic": 3400,
|
||||
"Jungle": 2500,
|
||||
"Mountain": 3700,
|
||||
"Forest": 2400,
|
||||
"Urban": 3500,
|
||||
"Combat": 4100,
|
||||
"Assault": 4300,
|
||||
"Recon": 2300,
|
||||
"Spec Ops": 4400,
|
||||
|
||||
# Limited Armors
|
||||
"Martial (L)": 13000,
|
||||
"Mayhem (L)": 13300,
|
||||
"Angel (L)": 14000,
|
||||
"Perseus (L)": 15000,
|
||||
"Moonshine (L)": 15400,
|
||||
"Eon (L)": 12000,
|
||||
"Hermes (L)": 12500,
|
||||
"Tiger (L)": 11000,
|
||||
"Spartacus (L)": 11500,
|
||||
"Vain (L)": 11800,
|
||||
}
|
||||
|
||||
# Default durability for unknown armors
|
||||
DEFAULT_DURABILITY = 2000 # Same as Ghost
|
||||
|
||||
|
||||
def calculate_armor_decay(damage_absorbed: Decimal, durability: int) -> Decimal:
|
||||
"""Calculate armor decay in PED.
|
||||
|
||||
Formula: Decay = damage * 0.05 * (1 - durability/100000)
|
||||
|
||||
Args:
|
||||
damage_absorbed: Amount of damage absorbed by armor (in HP)
|
||||
durability: Armor durability stat
|
||||
|
||||
Returns:
|
||||
Decay cost in PED
|
||||
"""
|
||||
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
|
||||
decay_pec = damage_absorbed * Decimal("0.05") * durability_factor
|
||||
return decay_pec / Decimal(100) # Convert PEC to PED
|
||||
|
||||
|
||||
def calculate_hp_per_pec(durability: int) -> Decimal:
|
||||
"""Calculate armor economy in hp/pec.
|
||||
|
||||
Args:
|
||||
durability: Armor durability stat
|
||||
|
||||
Returns:
|
||||
Economy rating in hp/pec (higher is better)
|
||||
"""
|
||||
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
|
||||
return Decimal("20") / durability_factor
|
||||
|
||||
|
||||
def calculate_protection_cost_per_100_ped(durability: int) -> int:
|
||||
"""Calculate how much damage 100 PED of decay will absorb.
|
||||
|
||||
Args:
|
||||
durability: Armor durability stat
|
||||
|
||||
Returns:
|
||||
Damage absorbed per 100 PED decay
|
||||
"""
|
||||
hp_per_pec = calculate_hp_per_pec(durability)
|
||||
return int(hp_per_pec * 10000) # 100 PED = 10,000 PEC
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArmorPiece:
|
||||
"""Represents a single armor piece."""
|
||||
name: str
|
||||
slot: str # 'head', 'chest', 'arms', 'hands', 'legs', 'feet'
|
||||
durability: int
|
||||
protection_impact: Decimal = Decimal("0")
|
||||
protection_cut: Decimal = Decimal("0")
|
||||
protection_stab: Decimal = Decimal("0")
|
||||
protection_burn: Decimal = Decimal("0")
|
||||
protection_cold: Decimal = Decimal("0")
|
||||
protection_acid: Decimal = Decimal("0")
|
||||
protection_electric: Decimal = Decimal("0")
|
||||
|
||||
def calculate_decay(self, damage_absorbed: Decimal) -> Decimal:
|
||||
"""Calculate decay for this piece."""
|
||||
return calculate_armor_decay(damage_absorbed, self.durability)
|
||||
|
||||
def get_economy(self) -> Decimal:
|
||||
"""Get hp/pec economy rating."""
|
||||
return calculate_hp_per_pec(self.durability)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArmorSet:
|
||||
"""Represents a complete armor set (7 pieces)."""
|
||||
name: str
|
||||
head: Optional[ArmorPiece] = None
|
||||
chest: Optional[ArmorPiece] = None
|
||||
left_arm: Optional[ArmorPiece] = None
|
||||
right_arm: Optional[ArmorPiece] = None
|
||||
left_hand: Optional[ArmorPiece] = None
|
||||
right_hand: Optional[ArmorPiece] = None
|
||||
legs: Optional[ArmorPiece] = None
|
||||
feet: Optional[ArmorPiece] = None
|
||||
|
||||
def get_all_pieces(self) -> list:
|
||||
"""Get list of all equipped pieces."""
|
||||
pieces = []
|
||||
for piece in [self.head, self.chest, self.left_arm, self.right_arm,
|
||||
self.left_hand, self.right_hand, self.legs, self.feet]:
|
||||
if piece:
|
||||
pieces.append(piece)
|
||||
return pieces
|
||||
|
||||
def get_total_protection(self, damage_type: str = "impact") -> Decimal:
|
||||
"""Get total protection for a damage type."""
|
||||
total = Decimal("0")
|
||||
for piece in self.get_all_pieces():
|
||||
protection = getattr(piece, f"protection_{damage_type}", Decimal("0"))
|
||||
total += protection
|
||||
return total
|
||||
|
||||
def calculate_total_decay(self, damage_per_piece: Dict[str, Decimal]) -> Decimal:
|
||||
"""Calculate total decay for all pieces.
|
||||
|
||||
Args:
|
||||
damage_per_piece: Dict mapping slot names to damage absorbed
|
||||
|
||||
Returns:
|
||||
Total decay in PED
|
||||
"""
|
||||
total_decay = Decimal("0")
|
||||
for piece in self.get_all_pieces():
|
||||
if piece.slot in damage_per_piece:
|
||||
damage = damage_per_piece[piece.slot]
|
||||
total_decay += piece.calculate_decay(damage)
|
||||
return total_decay
|
||||
|
||||
|
||||
def get_armor_durability(armor_name: str) -> int:
|
||||
"""Get durability for an armor by name.
|
||||
|
||||
Args:
|
||||
armor_name: Name of the armor
|
||||
|
||||
Returns:
|
||||
Durability value (defaults to 2000 if unknown)
|
||||
"""
|
||||
return ARMOR_DURABILITY.get(armor_name, DEFAULT_DURABILITY)
|
||||
|
||||
|
||||
def compare_armor_economy(armor_names: list) -> list:
|
||||
"""Compare economy of multiple armors.
|
||||
|
||||
Args:
|
||||
armor_names: List of armor names to compare
|
||||
|
||||
Returns:
|
||||
List of tuples (name, durability, hp_per_pec, dmg_per_100ped)
|
||||
"""
|
||||
results = []
|
||||
for name in armor_names:
|
||||
durability = get_armor_durability(name)
|
||||
hp_per_pec = calculate_hp_per_pec(durability)
|
||||
dmg_per_100 = calculate_protection_cost_per_100_ped(durability)
|
||||
results.append((name, durability, hp_per_pec, dmg_per_100))
|
||||
|
||||
# Sort by economy (hp/pec) descending
|
||||
results.sort(key=lambda x: x[2], reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Compare popular armors
|
||||
armors = ["Ghost", "Gremlin", "Adjusted Nemesis", "Angel",
|
||||
"Martial (L)", "Angel (L)", "Perseus (L)"]
|
||||
|
||||
print("Armor Economy Comparison:")
|
||||
print("-" * 70)
|
||||
print(f"{'Armor':<25} {'Dur':<8} {'hp/pec':<12} {'dmg/100PED':<12}")
|
||||
print("-" * 70)
|
||||
|
||||
for name, durability, hp_per_pec, dmg_per_100 in compare_armor_economy(armors):
|
||||
print(f"{name:<25} {durability:<8} {hp_per_pec:<12.2f} {dmg_per_100:<12,}")
|
||||
|
||||
# Example decay calculation
|
||||
print("\n\nDecay Example (15 damage absorbed):")
|
||||
print("-" * 50)
|
||||
for armor in ["Ghost", "Angel", "Martial (L)"]:
|
||||
durability = get_armor_durability(armor)
|
||||
decay = calculate_armor_decay(Decimal("15"), durability)
|
||||
print(f"{armor:<20} {decay:.5f} PED")
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
# Standards: Python 3.11+, type hints, Decimal precision for PED/PEC
|
||||
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass, asdict, field
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
|
@ -45,6 +45,204 @@ class SessionData:
|
|||
notes: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HuntingSessionData:
|
||||
"""
|
||||
Extended data class for hunting sessions with detailed metrics.
|
||||
|
||||
This class tracks all hunting-specific statistics including:
|
||||
- Loot breakdown (shrapnel, universal ammo, other loot)
|
||||
- Cost breakdown (weapon, armor, healing, plates)
|
||||
- Combat statistics (damage, kills, shots fired)
|
||||
- Special events (globals, HoFs)
|
||||
- Calculated efficiency metrics (DPP, return %, cost/kill)
|
||||
"""
|
||||
id: Optional[int] = None
|
||||
session_id: Optional[int] = None
|
||||
|
||||
# Timestamps
|
||||
started_at: Optional[datetime] = None
|
||||
ended_at: Optional[datetime] = None
|
||||
|
||||
# Loot tracking (PED values)
|
||||
total_loot_ped: Decimal = Decimal("0.0")
|
||||
total_shrapnel_ped: Decimal = Decimal("0.0")
|
||||
total_universal_ammo_ped: Decimal = Decimal("0.0")
|
||||
total_other_loot_ped: Decimal = Decimal("0.0") # Actual marketable loot
|
||||
|
||||
# Cost tracking (PED values)
|
||||
weapon_cost_ped: Decimal = Decimal("0.0")
|
||||
armor_cost_ped: Decimal = Decimal("0.0")
|
||||
healing_cost_ped: Decimal = Decimal("0.0")
|
||||
plates_cost_ped: Decimal = Decimal("0.0")
|
||||
enhancer_cost_ped: Decimal = Decimal("0.0")
|
||||
total_cost_ped: Decimal = Decimal("0.0")
|
||||
|
||||
# Combat tracking
|
||||
damage_dealt: Decimal = Decimal("0.0")
|
||||
damage_taken: Decimal = Decimal("0.0")
|
||||
healing_done: Decimal = Decimal("0.0")
|
||||
shots_fired: int = 0
|
||||
shots_missed: int = 0
|
||||
evades: int = 0
|
||||
kills: int = 0
|
||||
|
||||
# Special events
|
||||
globals_count: int = 0
|
||||
hofs_count: int = 0
|
||||
personal_globals: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
# Skill gains (tracked separately)
|
||||
skill_gains: Dict[str, Decimal] = field(default_factory=dict)
|
||||
|
||||
# Equipment used
|
||||
weapon_name: str = ""
|
||||
weapon_dpp: Decimal = Decimal("0.0")
|
||||
armor_name: str = ""
|
||||
fap_name: str = ""
|
||||
|
||||
# Calculated properties
|
||||
@property
|
||||
def net_profit_ped(self) -> Decimal:
|
||||
"""Net profit (excluding shrapnel from loot value)."""
|
||||
return self.total_other_loot_ped - self.total_cost_ped
|
||||
|
||||
@property
|
||||
def return_percentage(self) -> Decimal:
|
||||
"""Return percentage (other_loot / cost * 100)."""
|
||||
if self.total_cost_ped > 0:
|
||||
return (self.total_other_loot_ped / self.total_cost_ped) * Decimal('100')
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def return_percentage_with_shrapnel(self) -> Decimal:
|
||||
"""Return percentage including shrapnel."""
|
||||
if self.total_cost_ped > 0:
|
||||
return (self.total_loot_ped / self.total_cost_ped) * Decimal('100')
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def cost_per_kill(self) -> Decimal:
|
||||
"""Average cost per kill."""
|
||||
if self.kills > 0:
|
||||
return self.total_cost_ped / self.kills
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def cost_per_damage(self) -> Decimal:
|
||||
"""Cost per damage point (PED/damage)."""
|
||||
if self.damage_dealt > 0:
|
||||
return self.total_cost_ped / self.damage_dealt
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def dpp(self) -> Decimal:
|
||||
"""Damage Per PED - efficiency metric."""
|
||||
if self.total_cost_ped > 0:
|
||||
return self.damage_dealt / self.total_cost_ped
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def damage_per_kill(self) -> Decimal:
|
||||
"""Average damage per kill."""
|
||||
if self.kills > 0:
|
||||
return self.damage_dealt / self.kills
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def loot_per_kill(self) -> Decimal:
|
||||
"""Average loot per kill."""
|
||||
if self.kills > 0:
|
||||
return self.total_other_loot_ped / self.kills
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def accuracy(self) -> Decimal:
|
||||
"""Hit accuracy percentage."""
|
||||
total_shots = self.shots_fired + self.shots_missed
|
||||
if total_shots > 0:
|
||||
return (self.shots_fired / total_shots) * Decimal('100')
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def session_duration(self) -> Optional[timedelta]:
|
||||
"""Calculate session duration."""
|
||||
if self.started_at and self.ended_at:
|
||||
return self.ended_at - self.started_at
|
||||
elif self.started_at:
|
||||
return datetime.now() - self.started_at
|
||||
return None
|
||||
|
||||
@property
|
||||
def kills_per_hour(self) -> Decimal:
|
||||
"""Kill rate per hour."""
|
||||
duration = self.session_duration
|
||||
if duration and duration.total_seconds() > 0:
|
||||
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
|
||||
return Decimal(str(self.kills)) / hours
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def cost_per_hour(self) -> Decimal:
|
||||
"""Cost rate per hour."""
|
||||
duration = self.session_duration
|
||||
if duration and duration.total_seconds() > 0:
|
||||
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
|
||||
return self.total_cost_ped / hours
|
||||
return Decimal('0.0')
|
||||
|
||||
@property
|
||||
def profit_per_hour(self) -> Decimal:
|
||||
"""Profit rate per hour."""
|
||||
duration = self.session_duration
|
||||
if duration and duration.total_seconds() > 0:
|
||||
hours = Decimal(str(duration.total_seconds())) / Decimal('3600')
|
||||
return self.net_profit_ped / hours
|
||||
return Decimal('0.0')
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'session_id': self.session_id,
|
||||
'started_at': self.started_at.isoformat() if self.started_at else None,
|
||||
'ended_at': self.ended_at.isoformat() if self.ended_at else None,
|
||||
'total_loot_ped': float(self.total_loot_ped),
|
||||
'total_shrapnel_ped': float(self.total_shrapnel_ped),
|
||||
'total_universal_ammo_ped': float(self.total_universal_ammo_ped),
|
||||
'total_other_loot_ped': float(self.total_other_loot_ped),
|
||||
'weapon_cost_ped': float(self.weapon_cost_ped),
|
||||
'armor_cost_ped': float(self.armor_cost_ped),
|
||||
'healing_cost_ped': float(self.healing_cost_ped),
|
||||
'plates_cost_ped': float(self.plates_cost_ped),
|
||||
'enhancer_cost_ped': float(self.enhancer_cost_ped),
|
||||
'total_cost_ped': float(self.total_cost_ped),
|
||||
'damage_dealt': float(self.damage_dealt),
|
||||
'damage_taken': float(self.damage_taken),
|
||||
'healing_done': float(self.healing_done),
|
||||
'shots_fired': self.shots_fired,
|
||||
'shots_missed': self.shots_missed,
|
||||
'evades': self.evades,
|
||||
'kills': self.kills,
|
||||
'globals_count': self.globals_count,
|
||||
'hofs_count': self.hofs_count,
|
||||
'net_profit_ped': float(self.net_profit_ped),
|
||||
'return_percentage': float(self.return_percentage),
|
||||
'return_percentage_with_shrapnel': float(self.return_percentage_with_shrapnel),
|
||||
'cost_per_kill': float(self.cost_per_kill),
|
||||
'dpp': float(self.dpp),
|
||||
'damage_per_kill': float(self.damage_per_kill),
|
||||
'loot_per_kill': float(self.loot_per_kill),
|
||||
'kills_per_hour': float(self.kills_per_hour),
|
||||
'cost_per_hour': float(self.cost_per_hour),
|
||||
'profit_per_hour': float(self.profit_per_hour),
|
||||
'weapon_name': self.weapon_name,
|
||||
'weapon_dpp': float(self.weapon_dpp),
|
||||
'armor_name': self.armor_name,
|
||||
'fap_name': self.fap_name,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LootEvent:
|
||||
"""Data class representing a loot event from chat.log."""
|
||||
|
|
@ -55,12 +253,28 @@ class LootEvent:
|
|||
item_name: Optional[str] = None
|
||||
quantity: int = 1
|
||||
value_ped: Decimal = Decimal("0.0")
|
||||
is_shrapnel: bool = False
|
||||
is_universal_ammo: bool = False
|
||||
creature_name: Optional[str] = None
|
||||
zone_name: Optional[str] = None
|
||||
raw_log_line: Optional[str] = None
|
||||
screenshot_path: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CombatEvent:
|
||||
"""Data class representing a combat event."""
|
||||
id: Optional[int] = None
|
||||
session_id: Optional[int] = None
|
||||
timestamp: Optional[datetime] = None
|
||||
event_type: str = "damage_dealt" # damage_dealt, damage_taken, heal, evade, kill
|
||||
damage_amount: Optional[Decimal] = None
|
||||
heal_amount: Optional[Decimal] = None
|
||||
creature_name: Optional[str] = None
|
||||
weapon_name: Optional[str] = None
|
||||
raw_log_line: Optional[str] = None
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
"""
|
||||
Manages Projects implementing the Data Principle.
|
||||
|
|
@ -74,6 +288,7 @@ class ProjectManager:
|
|||
db: DatabaseManager instance for persistence
|
||||
current_project: Currently active ProjectData
|
||||
current_session: Currently active SessionData
|
||||
current_hunting_session: Currently active HuntingSessionData
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[DatabaseManager] = None):
|
||||
|
|
@ -86,6 +301,7 @@ class ProjectManager:
|
|||
self.db = db if db else DatabaseManager()
|
||||
self.current_project: Optional[ProjectData] = None
|
||||
self.current_session: Optional[SessionData] = None
|
||||
self.current_hunting_session: Optional[HuntingSessionData] = None
|
||||
|
||||
logger.info("ProjectManager initialized")
|
||||
|
||||
|
|
@ -270,6 +486,62 @@ class ProjectManager:
|
|||
logger.info(f"Started session {session.id} for project {project_id}")
|
||||
return session
|
||||
|
||||
def start_hunting_session(self, project_id: int,
|
||||
weapon_name: str = "",
|
||||
weapon_dpp: Decimal = Decimal('0.0'),
|
||||
armor_name: str = "",
|
||||
fap_name: str = "") -> HuntingSessionData:
|
||||
"""
|
||||
Start a new hunting session with full tracking.
|
||||
|
||||
Args:
|
||||
project_id: Parent project ID
|
||||
weapon_name: Name of equipped weapon
|
||||
weapon_dpp: Weapon DPP rating
|
||||
armor_name: Name of equipped armor
|
||||
fap_name: Name of equipped FAP
|
||||
|
||||
Returns:
|
||||
Created HuntingSessionData
|
||||
"""
|
||||
# First create base session
|
||||
base_session = self.start_session(project_id, notes=f"Hunting with {weapon_name}")
|
||||
|
||||
# Create hunting session data
|
||||
hunting_session = HuntingSessionData(
|
||||
session_id=base_session.id,
|
||||
started_at=datetime.now(),
|
||||
weapon_name=weapon_name,
|
||||
weapon_dpp=weapon_dpp,
|
||||
armor_name=armor_name,
|
||||
fap_name=fap_name
|
||||
)
|
||||
|
||||
self.current_hunting_session = hunting_session
|
||||
|
||||
# Insert into hunting_sessions table
|
||||
cursor = self.db.execute(
|
||||
"""
|
||||
INSERT INTO hunting_sessions
|
||||
(session_id, started_at, weapon_name, weapon_dpp, armor_name, fap_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
base_session.id,
|
||||
hunting_session.started_at,
|
||||
weapon_name,
|
||||
float(weapon_dpp),
|
||||
armor_name,
|
||||
fap_name
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
hunting_session.id = cursor.lastrowid
|
||||
|
||||
logger.info(f"Started hunting session {hunting_session.id} with {weapon_name}")
|
||||
return hunting_session
|
||||
|
||||
def end_session(self, session_id: Optional[int] = None) -> bool:
|
||||
"""
|
||||
End a session and calculate final metrics.
|
||||
|
|
@ -287,6 +559,10 @@ class ProjectManager:
|
|||
logger.error("No session ID provided")
|
||||
return False
|
||||
|
||||
# If we have an active hunting session, finalize it first
|
||||
if self.current_hunting_session and self.current_hunting_session.session_id == session_id:
|
||||
self._finalize_hunting_session()
|
||||
|
||||
# Calculate metrics
|
||||
cursor = self.db.execute(
|
||||
"""
|
||||
|
|
@ -331,6 +607,115 @@ class ProjectManager:
|
|||
logger.info(f"Ended session {session_id}. Profit: {net_profit} PED")
|
||||
return True
|
||||
|
||||
def _finalize_hunting_session(self):
|
||||
"""Finalize the current hunting session in the database."""
|
||||
if not self.current_hunting_session:
|
||||
return
|
||||
|
||||
hs = self.current_hunting_session
|
||||
hs.ended_at = datetime.now()
|
||||
|
||||
self.db.execute(
|
||||
"""
|
||||
UPDATE hunting_sessions SET
|
||||
ended_at = ?,
|
||||
total_loot_ped = ?,
|
||||
total_shrapnel_ped = ?,
|
||||
total_universal_ammo_ped = ?,
|
||||
total_other_loot_ped = ?,
|
||||
weapon_cost_ped = ?,
|
||||
armor_cost_ped = ?,
|
||||
healing_cost_ped = ?,
|
||||
plates_cost_ped = ?,
|
||||
total_cost_ped = ?,
|
||||
damage_dealt = ?,
|
||||
damage_taken = ?,
|
||||
healing_done = ?,
|
||||
shots_fired = ?,
|
||||
shots_missed = ?,
|
||||
evades = ?,
|
||||
kills = ?,
|
||||
globals_count = ?,
|
||||
hofs_count = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
hs.ended_at,
|
||||
float(hs.total_loot_ped),
|
||||
float(hs.total_shrapnel_ped),
|
||||
float(hs.total_universal_ammo_ped),
|
||||
float(hs.total_other_loot_ped),
|
||||
float(hs.weapon_cost_ped),
|
||||
float(hs.armor_cost_ped),
|
||||
float(hs.healing_cost_ped),
|
||||
float(hs.plates_cost_ped),
|
||||
float(hs.total_cost_ped),
|
||||
float(hs.damage_dealt),
|
||||
float(hs.damage_taken),
|
||||
float(hs.healing_done),
|
||||
hs.shots_fired,
|
||||
hs.shots_missed,
|
||||
hs.evades,
|
||||
hs.kills,
|
||||
hs.globals_count,
|
||||
hs.hofs_count,
|
||||
hs.id
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
# Store personal globals
|
||||
for global_data in hs.personal_globals:
|
||||
self.db.execute(
|
||||
"""
|
||||
INSERT INTO hunting_globals
|
||||
(hunting_session_id, timestamp, creature_name, value_ped, is_hof)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
hs.id,
|
||||
global_data.get('timestamp', datetime.now()),
|
||||
global_data.get('creature', 'Unknown'),
|
||||
float(global_data.get('value_ped', Decimal('0.0'))),
|
||||
global_data.get('is_hof', False)
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Finalized hunting session {hs.id}")
|
||||
|
||||
def update_hunting_session(self, updates: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Update the current hunting session with new data.
|
||||
|
||||
Args:
|
||||
updates: Dictionary of field updates
|
||||
|
||||
Returns:
|
||||
True if updated successfully
|
||||
"""
|
||||
if not self.current_hunting_session:
|
||||
logger.error("No active hunting session to update")
|
||||
return False
|
||||
|
||||
hs = self.current_hunting_session
|
||||
|
||||
# Update fields
|
||||
for key, value in updates.items():
|
||||
if hasattr(hs, key):
|
||||
setattr(hs, key, value)
|
||||
|
||||
# Recalculate totals
|
||||
hs.total_cost_ped = (
|
||||
hs.weapon_cost_ped +
|
||||
hs.armor_cost_ped +
|
||||
hs.healing_cost_ped +
|
||||
hs.plates_cost_ped +
|
||||
hs.enhancer_cost_ped
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
# ========================================================================
|
||||
# LOOT TRACKING
|
||||
# ========================================================================
|
||||
|
|
@ -380,6 +765,44 @@ class ProjectManager:
|
|||
|
||||
return True
|
||||
|
||||
def record_combat_event(self, event: CombatEvent) -> bool:
|
||||
"""
|
||||
Record a combat event.
|
||||
|
||||
Args:
|
||||
event: CombatEvent data
|
||||
|
||||
Returns:
|
||||
True if recorded successfully
|
||||
"""
|
||||
if not self.current_session:
|
||||
logger.error("No active session to record combat event")
|
||||
return False
|
||||
|
||||
event.session_id = self.current_session.id
|
||||
|
||||
self.db.execute(
|
||||
"""
|
||||
INSERT INTO combat_events
|
||||
(session_id, timestamp, event_type, damage_amount, heal_amount,
|
||||
creature_name, weapon_name, raw_log_line)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
event.session_id,
|
||||
event.timestamp or datetime.now(),
|
||||
event.event_type,
|
||||
float(event.damage_amount) if event.damage_amount else None,
|
||||
float(event.heal_amount) if event.heal_amount else None,
|
||||
event.creature_name,
|
||||
event.weapon_name,
|
||||
event.raw_log_line
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
return True
|
||||
|
||||
def _trigger_screenshot(self, loot: LootEvent) -> None:
|
||||
"""
|
||||
Trigger screenshot capture for high-value loot.
|
||||
|
|
@ -426,6 +849,99 @@ class ProjectManager:
|
|||
'hof_count': row['hof_count']
|
||||
}
|
||||
|
||||
def get_hunting_session_summary(self, hunting_session_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get detailed summary for a hunting session.
|
||||
|
||||
Args:
|
||||
hunting_session_id: Hunting session ID
|
||||
|
||||
Returns:
|
||||
Dictionary with hunting session summary
|
||||
"""
|
||||
cursor = self.db.execute(
|
||||
"SELECT * FROM hunting_sessions WHERE id = ?",
|
||||
(hunting_session_id,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
# Build hunting session data
|
||||
hs = HuntingSessionData(
|
||||
id=row['id'],
|
||||
session_id=row['session_id'],
|
||||
started_at=row['started_at'],
|
||||
ended_at=row['ended_at'],
|
||||
total_loot_ped=Decimal(str(row['total_loot_ped'])),
|
||||
total_shrapnel_ped=Decimal(str(row['total_shrapnel_ped'])),
|
||||
total_universal_ammo_ped=Decimal(str(row['total_universal_ammo_ped'])),
|
||||
total_other_loot_ped=Decimal(str(row['total_other_loot_ped'])),
|
||||
weapon_cost_ped=Decimal(str(row['weapon_cost_ped'])),
|
||||
armor_cost_ped=Decimal(str(row['armor_cost_ped'])),
|
||||
healing_cost_ped=Decimal(str(row['healing_cost_ped'])),
|
||||
plates_cost_ped=Decimal(str(row['plates_cost_ped'])),
|
||||
total_cost_ped=Decimal(str(row['total_cost_ped'])),
|
||||
damage_dealt=Decimal(str(row['damage_dealt'])),
|
||||
damage_taken=Decimal(str(row['damage_taken'])),
|
||||
healing_done=Decimal(str(row['healing_done'])),
|
||||
shots_fired=row['shots_fired'],
|
||||
shots_missed=row['shots_missed'],
|
||||
evades=row['evades'],
|
||||
kills=row['kills'],
|
||||
globals_count=row['globals_count'],
|
||||
hofs_count=row['hofs_count'],
|
||||
weapon_name=row['weapon_name'],
|
||||
weapon_dpp=Decimal(str(row['weapon_dpp'])),
|
||||
armor_name=row['armor_name'],
|
||||
fap_name=row['fap_name']
|
||||
)
|
||||
|
||||
return hs.to_dict()
|
||||
|
||||
def get_hunting_history(self, project_id: int, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get hunting session history for a project.
|
||||
|
||||
Args:
|
||||
project_id: Project ID
|
||||
limit: Maximum number of sessions to return
|
||||
|
||||
Returns:
|
||||
List of hunting session summaries
|
||||
"""
|
||||
cursor = self.db.execute(
|
||||
"""
|
||||
SELECT hs.* FROM hunting_sessions hs
|
||||
JOIN sessions s ON hs.session_id = s.id
|
||||
WHERE s.project_id = ?
|
||||
ORDER BY hs.started_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(project_id, limit)
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
history = []
|
||||
for row in rows:
|
||||
hs = HuntingSessionData(
|
||||
id=row['id'],
|
||||
session_id=row['session_id'],
|
||||
started_at=row['started_at'],
|
||||
ended_at=row['ended_at'],
|
||||
total_loot_ped=Decimal(str(row['total_loot_ped'])),
|
||||
total_other_loot_ped=Decimal(str(row['total_other_loot_ped'])),
|
||||
total_cost_ped=Decimal(str(row['total_cost_ped'])),
|
||||
kills=row['kills'],
|
||||
globals_count=row['globals_count'],
|
||||
hofs_count=row['hofs_count'],
|
||||
weapon_name=row['weapon_name']
|
||||
)
|
||||
history.append(hs.to_dict())
|
||||
|
||||
return history
|
||||
|
||||
def compare_to_historical(self, project_id: int,
|
||||
metric: str = 'net_profit') -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
@ -485,5 +1001,7 @@ __all__ = [
|
|||
'ProjectManager',
|
||||
'ProjectData',
|
||||
'SessionData',
|
||||
'LootEvent'
|
||||
'HuntingSessionData',
|
||||
'LootEvent',
|
||||
'CombatEvent'
|
||||
]
|
||||
|
|
|
|||
155
core/schema.sql
155
core/schema.sql
|
|
@ -1,7 +1,8 @@
|
|||
-- Description: SQLite database schema for Lemontropia Suite
|
||||
-- Implements the Data Principle: Every session is a Project
|
||||
-- Schema version: 1.0.0
|
||||
-- Schema version: 2.0.0 - Added comprehensive hunting session tracking
|
||||
-- Created: 2026-02-08
|
||||
-- Updated: 2026-02-09
|
||||
|
||||
-- Enable foreign key support
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
|
@ -45,6 +46,73 @@ 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);
|
||||
|
||||
-- ============================================================================
|
||||
-- HUNTING SESSIONS (Extended tracking for hunting activities)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hunting_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL UNIQUE,
|
||||
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TIMESTAMP,
|
||||
|
||||
-- Loot breakdown
|
||||
total_loot_ped REAL DEFAULT 0.0,
|
||||
total_shrapnel_ped REAL DEFAULT 0.0,
|
||||
total_universal_ammo_ped REAL DEFAULT 0.0,
|
||||
total_other_loot_ped REAL DEFAULT 0.0, -- Marketable loot excluding shrapnel/UA
|
||||
|
||||
-- Cost breakdown
|
||||
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,
|
||||
total_cost_ped REAL DEFAULT 0.0,
|
||||
|
||||
-- Combat statistics
|
||||
damage_dealt REAL DEFAULT 0.0,
|
||||
damage_taken REAL DEFAULT 0.0,
|
||||
healing_done REAL DEFAULT 0.0,
|
||||
shots_fired INTEGER DEFAULT 0,
|
||||
shots_missed INTEGER DEFAULT 0,
|
||||
evades INTEGER DEFAULT 0,
|
||||
kills INTEGER DEFAULT 0,
|
||||
|
||||
-- Special events
|
||||
globals_count INTEGER DEFAULT 0,
|
||||
hofs_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Equipment used
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_hunting_sessions_session ON hunting_sessions(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_hunting_sessions_weapon ON hunting_sessions(weapon_name);
|
||||
|
||||
-- ============================================================================
|
||||
-- HUNTING GLOBALS/HoFs (Record of notable loot events)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hunting_globals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hunting_session_id INTEGER NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
creature_name TEXT,
|
||||
value_ped REAL NOT NULL,
|
||||
is_hof BOOLEAN DEFAULT 0,
|
||||
screenshot_path TEXT,
|
||||
FOREIGN KEY (hunting_session_id) REFERENCES hunting_sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_hunting_globals_session ON hunting_globals(hunting_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_hunting_globals_value ON hunting_globals(value_ped);
|
||||
|
||||
-- ============================================================================
|
||||
-- LOOT EVENTS (Core data capture)
|
||||
-- ============================================================================
|
||||
|
|
@ -59,6 +127,8 @@ CREATE TABLE IF NOT EXISTS loot_events (
|
|||
item_name TEXT,
|
||||
quantity INTEGER DEFAULT 1,
|
||||
value_ped REAL DEFAULT 0.0,
|
||||
is_shrapnel BOOLEAN DEFAULT 0,
|
||||
is_universal_ammo BOOLEAN DEFAULT 0,
|
||||
|
||||
-- Context
|
||||
creature_name TEXT, -- For hunter module
|
||||
|
|
@ -78,6 +148,29 @@ CREATE INDEX IF NOT EXISTS idx_loot_timestamp ON loot_events(timestamp);
|
|||
CREATE INDEX IF NOT EXISTS idx_loot_type ON loot_events(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_loot_value ON loot_events(value_ped);
|
||||
|
||||
-- ============================================================================
|
||||
-- COMBAT EVENTS (Detailed combat tracking)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS combat_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('damage_dealt', 'damage_taken', 'heal', 'evade', 'kill', 'critical_hit')),
|
||||
|
||||
damage_amount REAL,
|
||||
heal_amount REAL,
|
||||
creature_name TEXT,
|
||||
weapon_name TEXT,
|
||||
|
||||
raw_log_line TEXT,
|
||||
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_combat_session ON combat_events(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_combat_type ON combat_events(event_type);
|
||||
|
||||
-- ============================================================================
|
||||
-- SKILL GAINS (Character progression tracking)
|
||||
-- ============================================================================
|
||||
|
|
@ -106,6 +199,7 @@ CREATE TABLE IF NOT EXISTS decay_events (
|
|||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
item_name TEXT NOT NULL,
|
||||
decay_amount_ped REAL DEFAULT 0.0,
|
||||
decay_amount_pec REAL DEFAULT 0.0,
|
||||
shots_fired INTEGER DEFAULT 0,
|
||||
raw_log_line TEXT,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
|
|
@ -149,9 +243,9 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
|||
description TEXT
|
||||
);
|
||||
|
||||
-- Insert initial version
|
||||
INSERT OR IGNORE INTO schema_version (version, description)
|
||||
VALUES (1, 'Initial schema with Data Principle support');
|
||||
-- Insert/update version
|
||||
INSERT OR REPLACE INTO schema_version (version, description)
|
||||
VALUES (2, 'Added comprehensive hunting session tracking with loot/cost breakdown');
|
||||
|
||||
-- ============================================================================
|
||||
-- VIEWS FOR COMMON QUERIES (Performance optimization)
|
||||
|
|
@ -199,3 +293,56 @@ FROM sessions s
|
|||
JOIN projects p ON s.project_id = p.id
|
||||
LEFT JOIN loot_events l ON s.id = l.session_id
|
||||
GROUP BY s.id;
|
||||
|
||||
-- Hunting session detailed view
|
||||
CREATE VIEW IF NOT EXISTS v_hunting_session_summary AS
|
||||
SELECT
|
||||
hs.id,
|
||||
hs.session_id,
|
||||
s.project_id,
|
||||
p.name as project_name,
|
||||
hs.started_at,
|
||||
hs.ended_at,
|
||||
hs.total_loot_ped,
|
||||
hs.total_other_loot_ped,
|
||||
hs.total_cost_ped,
|
||||
hs.damage_dealt,
|
||||
hs.kills,
|
||||
hs.globals_count,
|
||||
hs.hofs_count,
|
||||
hs.weapon_name,
|
||||
hs.weapon_dpp,
|
||||
CASE
|
||||
WHEN hs.total_cost_ped > 0
|
||||
THEN ROUND((hs.total_other_loot_ped / hs.total_cost_ped) * 100, 2)
|
||||
ELSE 0
|
||||
END as return_percent,
|
||||
CASE
|
||||
WHEN hs.kills > 0
|
||||
THEN ROUND(hs.total_cost_ped / hs.kills, 4)
|
||||
ELSE 0
|
||||
END as cost_per_kill,
|
||||
CASE
|
||||
WHEN hs.total_cost_ped > 0
|
||||
THEN ROUND(hs.damage_dealt / hs.total_cost_ped, 2)
|
||||
ELSE 0
|
||||
END as dpp
|
||||
FROM hunting_sessions hs
|
||||
JOIN sessions s ON hs.session_id = s.id
|
||||
JOIN projects p ON s.project_id = p.id;
|
||||
|
||||
-- Weapon performance analysis
|
||||
CREATE VIEW IF NOT EXISTS v_weapon_performance AS
|
||||
SELECT
|
||||
weapon_name,
|
||||
COUNT(*) as sessions_count,
|
||||
AVG(total_other_loot_ped) as avg_loot,
|
||||
AVG(total_cost_ped) as avg_cost,
|
||||
AVG(CASE WHEN total_cost_ped > 0 THEN (total_other_loot_ped / total_cost_ped) * 100 ELSE 0 END) as avg_return_percent,
|
||||
AVG(dpp) as avg_dpp,
|
||||
SUM(kills) as total_kills,
|
||||
SUM(globals_count) as total_globals,
|
||||
SUM(hofs_count) as total_hofs
|
||||
FROM hunting_sessions
|
||||
WHERE weapon_name IS NOT NULL AND weapon_name != ''
|
||||
GROUP BY weapon_name;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
# Entropia Universe Armor Decay Formula
|
||||
|
||||
**Source:** "A Most Complete Guide to Armors (2020 Edition)"
|
||||
**Formula Author:** Hijacker27 (confirmed through testing)
|
||||
**Date:** 2026-02-09
|
||||
|
||||
---
|
||||
|
||||
## Armor Decay Formula (VU 15.15)
|
||||
|
||||
```
|
||||
Decay (in PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
|
||||
```
|
||||
|
||||
Where:
|
||||
- **damage_absorbed** = Amount of damage the armor piece absorbed
|
||||
- **durability** = Armor's durability stat (varies by armor type)
|
||||
- **Decay output is in PEC** (divide by 100 for PED)
|
||||
|
||||
---
|
||||
|
||||
## Armor Economy (hp/pec)
|
||||
|
||||
The economy can be calculated as:
|
||||
```
|
||||
hp/pec = 20 / (1 - durability/100000)
|
||||
```
|
||||
|
||||
Or simplified:
|
||||
```
|
||||
hp/pec ≈ 20 + (durability/5000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unlimited Armor Protection Costs
|
||||
|
||||
| Armor | Durability | hp/pec | dmg/100 PED decay |
|
||||
|-------|------------|--------|-------------------|
|
||||
| Ghost | 2000 | ~20.41 | 204,082 damage |
|
||||
| Gremlin | 2950 | ~20.61 | 206,079 damage |
|
||||
| Adjusted Nemesis | 3400 | ~20.70 | 207,039 damage |
|
||||
| Angel | 4000 | ~20.83 | 208,333 damage |
|
||||
|
||||
**Analysis:** Using Angel instead of Ghost saves ~2 PED for every 100 PED of decay (2% savings).
|
||||
|
||||
---
|
||||
|
||||
## Limited (L) Armor Protection Costs
|
||||
|
||||
**Note:** Formula may differ for Limited armors with >10k durability (research ongoing).
|
||||
|
||||
| Armor | Durability | hp/pec | dmg/100 PED | vs Ghost |
|
||||
|-------|------------|--------|-------------|----------|
|
||||
| Martial (L) | 13,000 | ~22.99 | 229,885 | ⮟ 11.22% less decay |
|
||||
| Mayhem (L) | 13,300 | ~23.07 | 230,680 | ⮟ 11.53% less decay |
|
||||
| Angel (L) | 14,000 | ~23.26 | 232,558 | ⮟ 12.24% less decay |
|
||||
| Perseus (L) | 15,000 | ~23.53 | 235,294 | ⮟ 13.27% less decay |
|
||||
| Moonshine (L) | 15,400 | ~23.64 | 236,407 | ⮟ 13.67% less decay |
|
||||
|
||||
**Key Insight:** Limited armors with high durability (13k+) have significantly better economy than unlimited armors.
|
||||
|
||||
---
|
||||
|
||||
## Calculation Examples
|
||||
|
||||
### Example 1: Ghost Armor (2000 durability)
|
||||
Monster hits for 15 Impact, armor absorbs all 15:
|
||||
```
|
||||
Decay = 15 * 0.05 * (1 - 2000/100000)
|
||||
Decay = 15 * 0.05 * 0.98
|
||||
Decay = 0.735 PEC
|
||||
Decay = 0.00735 PED
|
||||
```
|
||||
|
||||
### Example 2: Angel Armor (4000 durability)
|
||||
Same 15 Impact hit:
|
||||
```
|
||||
Decay = 15 * 0.05 * (1 - 4000/100000)
|
||||
Decay = 15 * 0.05 * 0.96
|
||||
Decay = 0.72 PEC
|
||||
Decay = 0.0072 PED
|
||||
```
|
||||
|
||||
**Savings:** 0.015 PEC (2% less decay than Ghost)
|
||||
|
||||
### Example 3: Martial (L) (13000 durability)
|
||||
Same 15 Impact hit:
|
||||
```
|
||||
Decay = 15 * 0.05 * (1 - 13000/100000)
|
||||
Decay = 15 * 0.05 * 0.87
|
||||
Decay = 0.6525 PEC
|
||||
Decay = 0.006525 PED
|
||||
```
|
||||
|
||||
**Savings:** 0.0825 PEC (11.22% less decay than Ghost)
|
||||
|
||||
---
|
||||
|
||||
## Implementation for Lemontropia Suite
|
||||
|
||||
### Python Implementation:
|
||||
```python
|
||||
from decimal import Decimal
|
||||
|
||||
def calculate_armor_decay(damage_absorbed: Decimal, durability: int) -> Decimal:
|
||||
"""Calculate armor decay in PED.
|
||||
|
||||
Args:
|
||||
damage_absorbed: Amount of damage absorbed by armor
|
||||
durability: Armor durability stat
|
||||
|
||||
Returns:
|
||||
Decay cost in PED
|
||||
"""
|
||||
durability_factor = Decimal(1) - Decimal(durability) / Decimal(100000)
|
||||
decay_pec = damage_absorbed * Decimal("0.05") * durability_factor
|
||||
return decay_pec / Decimal(100) # Convert PEC to PED
|
||||
|
||||
# Example usage
|
||||
decay = calculate_armor_decay(Decimal("15"), 2000) # Ghost absorbing 15 dmg
|
||||
print(f"Decay: {decay:.5f} PED") # 0.00735 PED
|
||||
```
|
||||
|
||||
### Armor Stats Database:
|
||||
```python
|
||||
ARMOR_DURABILITY = {
|
||||
"Ghost": 2000,
|
||||
"Gremlin": 2950,
|
||||
"Adjusted Nemesis": 3400,
|
||||
"Angel": 4000,
|
||||
"Martial (L)": 13000,
|
||||
"Mayhem (L)": 13300,
|
||||
"Angel (L)": 14000,
|
||||
"Perseus (L)": 15000,
|
||||
"Moonshine (L)": 15400,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plate Decay Formula
|
||||
|
||||
**Note:** Plate decay uses the same formula with plate's own durability.
|
||||
|
||||
```
|
||||
Plate Decay = damage_absorbed_by_plate * 0.05 * (1 - plate_durability/100000)
|
||||
```
|
||||
|
||||
**Important:** Plate and armor decay are calculated **independently** based on how much damage each absorbed.
|
||||
|
||||
---
|
||||
|
||||
## Research Notes
|
||||
|
||||
1. **Formula confirmed** by Hijacker27 and guide author through independent testing
|
||||
2. **Limited armors >10k durability** may use different formula (needs more research)
|
||||
3. **Decay is linear** per damage point absorbed (post-Loot 2.0)
|
||||
4. **No minimum decay** - always proportional to damage absorbed
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- PlanetCalypsoForum: "A Most Complete Guide to Armors (2020 Edition)"
|
||||
- Hijacker27's research on armor decay
|
||||
- VU 15.15 patch notes (Loot 2.0 armor changes)
|
||||
|
|
@ -178,6 +178,16 @@ class GearSelectorDialog(QDialog):
|
|||
header.resizeSection(2, 80)
|
||||
header.resizeSection(3, 80)
|
||||
header.resizeSection(4, 80)
|
||||
elif self.gear_type == "medical_tool":
|
||||
self.results_tree.setHeaderLabels(["Name", "Max Heal", "Decay", "Cost/Heal", "Cost/h"])
|
||||
header = self.results_tree.header()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
for i in range(1, 5):
|
||||
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
|
||||
header.resizeSection(1, 80)
|
||||
header.resizeSection(2, 80)
|
||||
header.resizeSection(3, 80)
|
||||
header.resizeSection(4, 80)
|
||||
|
||||
def load_data_async(self):
|
||||
"""Load data in background thread."""
|
||||
|
|
@ -193,6 +203,10 @@ class GearSelectorDialog(QDialog):
|
|||
self.loader_thread = FinderLoaderThread()
|
||||
self.loader_thread.finders_loaded.connect(self.on_data_loaded)
|
||||
self.loader_thread.error_occurred.connect(self.on_load_error)
|
||||
elif self.gear_type == "medical_tool":
|
||||
self.loader_thread = MedicalToolLoaderThread()
|
||||
self.loader_thread.medical_tools_loaded.connect(self.on_data_loaded)
|
||||
self.loader_thread.error_occurred.connect(self.on_load_error)
|
||||
else:
|
||||
return
|
||||
|
||||
|
|
@ -246,6 +260,17 @@ class GearSelectorDialog(QDialog):
|
|||
])
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, f)
|
||||
self.results_tree.addTopLevelItem(item)
|
||||
elif self.gear_type == "medical_tool":
|
||||
for m in items:
|
||||
item = QTreeWidgetItem([
|
||||
m.name,
|
||||
str(m.max_heal) if m.max_heal else "-",
|
||||
f"{m.decay:.2f}" if m.decay else "-",
|
||||
f"{m.cost_per_heal:.4f}" if m.cost_per_heal else "-",
|
||||
f"{m.cost_per_hour:.2f}" if m.cost_per_hour else "-"
|
||||
])
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, m)
|
||||
self.results_tree.addTopLevelItem(item)
|
||||
|
||||
def on_search(self):
|
||||
"""Search items."""
|
||||
|
|
@ -296,6 +321,15 @@ class GearSelectorDialog(QDialog):
|
|||
self.stats_layout.addRow("Depth:", QLabel(f"{item.depth}m"))
|
||||
self.stats_layout.addRow("Radius:", QLabel(f"{item.radius}m"))
|
||||
self.stats_layout.addRow("Decay:", QLabel(f"{item.decay} PEC"))
|
||||
elif self.gear_type == "medical_tool" and isinstance(item, MedicalTool):
|
||||
self.stats_layout.addRow("Name:", QLabel(item.name))
|
||||
self.stats_layout.addRow("Max Heal:", QLabel(f"{item.max_heal} HP" if item.max_heal else "-"))
|
||||
self.stats_layout.addRow("Min Heal:", QLabel(f"{item.min_heal} HP" if item.min_heal else "-"))
|
||||
self.stats_layout.addRow("Decay:", QLabel(f"{item.decay:.2f} PEC/use" if item.decay else "-"))
|
||||
self.stats_layout.addRow("Cost/Heal:", QLabel(f"{item.cost_per_heal:.4f} PED" if item.cost_per_heal else "-"))
|
||||
self.stats_layout.addRow("Cost/Hour:", QLabel(f"{item.cost_per_hour:.2f} PED" if item.cost_per_hour else "-"))
|
||||
if item.uses_per_minute:
|
||||
self.stats_layout.addRow("Uses/Min:", QLabel(str(item.uses_per_minute)))
|
||||
|
||||
def on_item_double_clicked(self, item, column):
|
||||
"""Handle double click."""
|
||||
|
|
@ -325,6 +359,18 @@ class GearSelectorDialog(QDialog):
|
|||
self.gear_selected.emit("finder", f.name, {
|
||||
'id': f.id, 'item_id': f.item_id, 'depth': float(f.depth), 'radius': float(f.radius),
|
||||
})
|
||||
elif self.gear_type == "medical_tool":
|
||||
m = self.selected_gear
|
||||
self.gear_selected.emit("medical_tool", m.name, {
|
||||
'id': m.id,
|
||||
'item_id': m.item_id,
|
||||
'max_heal': float(m.max_heal) if m.max_heal else 0,
|
||||
'min_heal': float(m.min_heal) if m.min_heal else 0,
|
||||
'decay': float(m.decay) if m.decay else 0,
|
||||
'cost_per_heal': float(m.cost_per_heal) if m.cost_per_heal else 0,
|
||||
'cost_per_hour': float(m.cost_per_hour) if m.cost_per_hour else 0,
|
||||
'uses_per_minute': m.uses_per_minute or 0,
|
||||
})
|
||||
self.accept()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -857,6 +857,7 @@ class LoadoutManagerDialog(QDialog):
|
|||
"""Main dialog for managing hunting loadouts with full API integration."""
|
||||
|
||||
loadout_saved = pyqtSignal(str) # Emitted when loadout is saved
|
||||
loadout_selected = pyqtSignal(object) # Emitted when loadout is selected for use
|
||||
|
||||
def __init__(self, parent=None, config_dir: Optional[str] = None):
|
||||
super().__init__(parent)
|
||||
|
|
@ -1086,6 +1087,9 @@ class LoadoutManagerDialog(QDialog):
|
|||
# Buttons
|
||||
self.save_btn = QPushButton("💾 Save Loadout")
|
||||
self.save_btn.setObjectName("saveButton")
|
||||
self.use_btn = QPushButton("✅ Use Loadout")
|
||||
self.use_btn.setObjectName("useButton")
|
||||
self.use_btn.setToolTip("Use this loadout for current session")
|
||||
self.load_btn = QPushButton("📂 Load Selected")
|
||||
self.delete_btn = QPushButton("🗑️ Delete")
|
||||
self.delete_btn.setObjectName("deleteButton")
|
||||
|
|
@ -1112,6 +1116,7 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
left_btn_layout = QHBoxLayout()
|
||||
left_btn_layout.addWidget(self.load_btn)
|
||||
left_btn_layout.addWidget(self.use_btn)
|
||||
left_btn_layout.addWidget(self.delete_btn)
|
||||
left_layout.addLayout(left_btn_layout)
|
||||
|
||||
|
|
@ -1296,6 +1301,7 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
# Buttons
|
||||
self.save_btn.clicked.connect(self._save_loadout)
|
||||
self.use_btn.clicked.connect(self._use_selected)
|
||||
self.load_btn.clicked.connect(self._load_selected)
|
||||
self.delete_btn.clicked.connect(self._delete_selected)
|
||||
self.new_btn.clicked.connect(self._new_loadout)
|
||||
|
|
@ -1686,6 +1692,37 @@ class LoadoutManagerDialog(QDialog):
|
|||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
|
||||
|
||||
def _use_selected(self):
|
||||
"""Use the selected loadout for the current session."""
|
||||
item = self.saved_list.currentItem()
|
||||
if not item:
|
||||
QMessageBox.information(self, "No Selection", "Please select a loadout to use")
|
||||
return
|
||||
|
||||
filepath = item.data(Qt.ItemDataRole.UserRole)
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
data = json.load(f)
|
||||
config = LoadoutConfig.from_dict(data)
|
||||
|
||||
self.current_loadout = config
|
||||
# Emit signal with the loadout for main window to use
|
||||
self.loadout_selected.emit(config)
|
||||
|
||||
QMessageBox.information(self, "Loadout Selected",
|
||||
f"Loadout '{config.name}' is now active for your session.\n\n"
|
||||
f"Weapon: {config.weapon_name}\n"
|
||||
f"Healing Tool: {config.heal_name}\n"
|
||||
f"Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
|
||||
|
||||
self.accept() # Close dialog
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load loadout: {str(e)}")
|
||||
|
||||
def _delete_selected(self):
|
||||
"""Delete the selected loadout."""
|
||||
item = self.saved_list.currentItem()
|
||||
|
|
|
|||
|
|
@ -562,6 +562,11 @@ class MainWindow(QMainWindow):
|
|||
select_finder_action.triggered.connect(lambda: self.on_select_gear("finder"))
|
||||
select_gear_menu.addAction(select_finder_action)
|
||||
|
||||
select_medical_action = QAction("&Medical Tool", self)
|
||||
select_medical_action.setShortcut("Ctrl+M")
|
||||
select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool"))
|
||||
select_gear_menu.addAction(select_medical_action)
|
||||
|
||||
tools_menu.addSeparator()
|
||||
|
||||
loadout_action = QAction("&Loadout Manager", self)
|
||||
|
|
@ -1345,8 +1350,25 @@ class MainWindow(QMainWindow):
|
|||
"""Open Loadout Manager dialog."""
|
||||
from ui.loadout_manager import LoadoutManagerDialog
|
||||
dialog = LoadoutManagerDialog(self)
|
||||
dialog.loadout_selected.connect(self.on_loadout_selected)
|
||||
dialog.exec()
|
||||
|
||||
def on_loadout_selected(self, loadout):
|
||||
"""Handle loadout selection from Loadout Manager."""
|
||||
self._selected_loadout = loadout
|
||||
self.log_info("Loadout", f"Selected loadout: {loadout.name}")
|
||||
|
||||
# Update selected gear from loadout
|
||||
if hasattr(loadout, 'weapon_name'):
|
||||
self._selected_weapon = loadout.weapon_name
|
||||
if hasattr(loadout, 'heal_cost_pec'):
|
||||
# Create medical tool stats from loadout heal cost
|
||||
self._selected_medical_tool = loadout.heal_name
|
||||
self._selected_medical_tool_stats = {
|
||||
'decay': float(loadout.heal_cost_pec),
|
||||
'cost_per_heal': float(loadout.heal_cost_pec) / 100.0, # Convert PEC to PED
|
||||
}
|
||||
|
||||
def on_select_gear(self, gear_type: str = "weapon"):
|
||||
"""Open Gear Selector dialog."""
|
||||
from ui.gear_selector import GearSelectorDialog
|
||||
|
|
@ -1370,6 +1392,11 @@ class MainWindow(QMainWindow):
|
|||
elif gear_type == "finder":
|
||||
self._selected_finder = name
|
||||
self._selected_finder_stats = stats
|
||||
elif gear_type == "medical_tool":
|
||||
self._selected_medical_tool = name
|
||||
self._selected_medical_tool_stats = stats
|
||||
if self.session_state == SessionState.RUNNING:
|
||||
self.hud.update_stats({'medical_tool': name})
|
||||
|
||||
def on_about(self):
|
||||
"""Show about dialog."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue