docs: add official armor guide from PlanetCalypsoForum 2020 edition
- Loot 2.0 armor changes (June 2017) - Correct decay mechanics (plate/armor independent) - Armor progression guide (Gremlin → Ghost → Adj Nemesis) - 20 hp/pec base economy standard - Damage absorption flow and formulas
This commit is contained in:
parent
5f01f2f763
commit
08aec368a9
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@ import re
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, Dict, Any, Optional
|
from typing import Callable, List, Dict, Any, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -25,6 +25,79 @@ class LogEvent:
|
||||||
data: Dict[str, Any]
|
data: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LootItem:
|
||||||
|
"""Represents a single loot item."""
|
||||||
|
name: str
|
||||||
|
quantity: int
|
||||||
|
value_ped: Decimal
|
||||||
|
is_shrapnel: bool = False
|
||||||
|
is_universal_ammo: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HuntingSessionStats:
|
||||||
|
"""Statistics for a hunting session tracked from chat.log."""
|
||||||
|
# Financial tracking
|
||||||
|
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') # Non-shrapnel, non-UA loot
|
||||||
|
|
||||||
|
# Cost tracking
|
||||||
|
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')
|
||||||
|
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
|
||||||
|
kills: int = 0
|
||||||
|
|
||||||
|
# Special events
|
||||||
|
globals_count: int = 0
|
||||||
|
hofs_count: int = 0
|
||||||
|
personal_globals: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Calculated metrics
|
||||||
|
@property
|
||||||
|
def net_profit_ped(self) -> Decimal:
|
||||||
|
"""Calculate net profit (excluding shrapnel from loot)."""
|
||||||
|
return self.total_other_loot_ped - self.total_cost_ped
|
||||||
|
|
||||||
|
@property
|
||||||
|
def return_percentage(self) -> Decimal:
|
||||||
|
"""Calculate return percentage (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 cost_per_kill(self) -> Decimal:
|
||||||
|
"""Calculate cost per kill."""
|
||||||
|
if self.kills > 0:
|
||||||
|
return self.total_cost_ped / self.kills
|
||||||
|
return Decimal('0.0')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dpp(self) -> Decimal:
|
||||||
|
"""Calculate 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:
|
||||||
|
"""Calculate average damage per kill."""
|
||||||
|
if self.kills > 0:
|
||||||
|
return self.damage_dealt / self.kills
|
||||||
|
return Decimal('0.0')
|
||||||
|
|
||||||
|
|
||||||
class LogWatcher:
|
class LogWatcher:
|
||||||
"""
|
"""
|
||||||
Watches Entropia Universe chat.log and notifies observers of events.
|
Watches Entropia Universe chat.log and notifies observers of events.
|
||||||
|
|
@ -56,6 +129,35 @@ class LogWatcher:
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# LOOT PATTERN WITHOUT VALUE (some items don't show value)
|
||||||
|
# English: "You received Animal Thyroid Oil x 5"
|
||||||
|
PATTERN_LOOT_NO_VALUE_EN = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]:?\s*\[?\]?\s*'
|
||||||
|
r'You\s+received\s+\[?([\w\s\-()]+?)\]?\s+x\s*(\d+)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
PATTERN_LOOT_NO_VALUE_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Du\s+fick\s+([\w\s\-()]+?)\s+x\s*(\d+)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
# KILL PATTERNS - "You killed" messages (for accurate kill counting)
|
||||||
|
# English: "You killed [Creature Name]"
|
||||||
|
# Swedish: "Du dödade [Creature Name]"
|
||||||
|
PATTERN_KILL_EN = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]:?\s*\[?\]?\s*'
|
||||||
|
r'You\s+killed\s+\[?([\w\s\-()]+?)\]?',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
PATTERN_KILL_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Du\s+dödade\s+\[?([\w\s\-()]+?)\]?',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
# GLOBAL PATTERNS (Other players)
|
# GLOBAL PATTERNS (Other players)
|
||||||
# English: "PlayerName globals in Zone for 150.00 PED"
|
# English: "PlayerName globals in Zone for 150.00 PED"
|
||||||
# Swedish: "PlayerName hittade en avsättning (Item) med ett värde av X PED"
|
# Swedish: "PlayerName hittade en avsättning (Item) med ett värde av X PED"
|
||||||
|
|
@ -92,6 +194,14 @@ class LogWatcher:
|
||||||
|
|
||||||
# HALL OF FAME PATTERNS
|
# HALL OF FAME PATTERNS
|
||||||
# Swedish: "...En post har lagts till i Hall of Fame!"
|
# Swedish: "...En post har lagts till i Hall of Fame!"
|
||||||
|
# English: "[Hall of Fame] Player killed a creature (Creature) for X PED"
|
||||||
|
PATTERN_HOF_EN = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[Hall\s+of\s+Fame\]\s+\[?\]?\s*'
|
||||||
|
r'([\w\s]+?)\s+killed\s+a\s+creature\s+\(([^)]+)\)\s+'
|
||||||
|
r'(?:for|with\s+a\s+value\s+of)\s+(\d+(?:\.\d+)?)\s+PED',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
PATTERN_HOF_MARKER = re.compile(
|
PATTERN_HOF_MARKER = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[\w+\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[\w+\]\s+\[?\]?\s*'
|
||||||
r'.*?Hall\s+of\s+Fame',
|
r'.*?Hall\s+of\s+Fame',
|
||||||
|
|
@ -123,6 +233,12 @@ class LogWatcher:
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PATTERN_LEVEL_UP_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Du\s+har\s+avancerat\s+till\s+nivå\s+(\d+)\s+i\s+([\w\s]+)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
# DAMAGE DEALT - Swedish & English
|
# DAMAGE DEALT - Swedish & English
|
||||||
# Swedish: "Du orsakade 13.5 poäng skada"
|
# Swedish: "Du orsakade 13.5 poäng skada"
|
||||||
# English: "You inflicted 4.4 points of damage"
|
# English: "You inflicted 4.4 points of damage"
|
||||||
|
|
@ -173,7 +289,7 @@ class LogWatcher:
|
||||||
# English: "You healed yourself 25.5 points"
|
# English: "You healed yourself 25.5 points"
|
||||||
PATTERN_HEAL_SV = re.compile(
|
PATTERN_HEAL_SV = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
r'Du\s+läkte\s+dig\s+jälv\s+(\d+(?:\.\d+)?)\s+poäng',
|
r'Du\s+läkte\s+dig\själv\s+(\d+(?:\.\d+)?)\s+poäng',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -188,7 +304,7 @@ class LogWatcher:
|
||||||
# English: "Your Piron PBP-17 (L) has reached tier 2.68"
|
# English: "Your Piron PBP-17 (L) has reached tier 2.68"
|
||||||
PATTERN_WEAPON_TIER_SV = re.compile(
|
PATTERN_WEAPON_TIER_SV = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+nivå\s+(\d+(?:\.\d+)?)',
|
r'Din\s+([\w\s\-()]+?)\s+har\snått\s+nivå\s+(\d+(?:\.\d+)?)',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -200,24 +316,43 @@ class LogWatcher:
|
||||||
|
|
||||||
# COMBAT EVADE/DODGE/MISS
|
# COMBAT EVADE/DODGE/MISS
|
||||||
# English: "You Evaded", "The target Evaded your attack", "The attack missed you"
|
# English: "You Evaded", "The target Evaded your attack", "The attack missed you"
|
||||||
PATTERN_EVADE = re.compile(
|
# Swedish: "Du undvek", "Målet undvek din attack"
|
||||||
|
PATTERN_EVADE_EN = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
r'(You\s+Evaded|You\s+dodged|The\s+target\s+Evaded\s+your\s+attack|The\s+target\s+Dodged|The\s+attack\s+missed\s+you)',
|
r'(You\s+Evaded|You\s+dodged|The\s+target\s+Evaded\s+your\s+attack|The\s+target\s+Dodged|The\s+attack\s+missed\s+you)',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PATTERN_EVADE_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'(Du\s+undvek|Målet\s+undvek\s+din\s+attack|Attacken\s+missade\s+dig)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
# DECAY (when weapon durability decreases)
|
# DECAY (when weapon durability decreases)
|
||||||
# English: "Your Omegaton M2100 has decayed 15 PEC"
|
# English: "Your Omegaton M2100 has decayed 15 PEC"
|
||||||
# Swedish: "Din Piron PBP-17 (L) har nått minimalt skick"
|
# Swedish: "Din Piron PBP-17 (L) har nått minimalt skick"
|
||||||
PATTERN_DECAY = re.compile(
|
PATTERN_DECAY_EN = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
r'(?:Your|Din)\s+([\w\s\-()]+?)\s+(?:has\s+decayed|har\s+nått\s+minimalt\s+skick)',
|
r'Your\s+([\w\s\-()]+?)\s+has\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
PATTERN_DECAY_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Din\s+([\w\s\-()]+?)\s+har\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
PATTERN_WEAPON_BROKEN_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+minimalt\s+skick',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
# BROKEN ENHANCERS
|
# BROKEN ENHANCERS
|
||||||
# English: "Your enhancer Weapon Damage Enhancer 1 on your Piron PBP-17 (L) broke"
|
# English: "Your enhancer Weapon Damage Enhancer 1 on your Piron PBP-17 (L) broke"
|
||||||
PATTERN_ENHANCER_BROKEN = re.compile(
|
PATTERN_ENHANCER_BROKEN_EN = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
r'Your\s+enhancer\s+([\w\s]+?)\s+on\s+your\s+([\w\s\-()]+?)\s+broke',
|
r'Your\s+enhancer\s+([\w\s]+?)\s+on\s+your\s+([\w\s\-()]+?)\s+broke',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
|
|
@ -225,12 +360,18 @@ class LogWatcher:
|
||||||
|
|
||||||
# PED TRANSFER
|
# PED TRANSFER
|
||||||
# Swedish: "Överföring slutförd! 3.38000 PED har överförts till ditt PED-kort."
|
# Swedish: "Överföring slutförd! 3.38000 PED har överförts till ditt PED-kort."
|
||||||
PATTERN_PED_TRANSFER = re.compile(
|
PATTERN_PED_TRANSFER_SV = re.compile(
|
||||||
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+'
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+'
|
||||||
r'Överföring\s+slutförd.*?((\d+(?:\.\d+)?))\s+PED',
|
r'Överföring\s+slutförd.*?((\d+(?:\.\d+)?))\s+PED',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PATTERN_PED_TRANSFER_EN = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+'
|
||||||
|
r'Transfer\s+complete.*?((\d+(?:\.\d+)?))\s+PED',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
# ATTRIBUTE GAIN (Agility, etc)
|
# ATTRIBUTE GAIN (Agility, etc)
|
||||||
# Swedish: "Din Agility har förbättrats med 0.0001"
|
# Swedish: "Din Agility har förbättrats med 0.0001"
|
||||||
# English: "Your Agility has improved by 0.0001" OR "You gained 0.0001 Agility"
|
# English: "Your Agility has improved by 0.0001" OR "You gained 0.0001 Agility"
|
||||||
|
|
@ -246,14 +387,33 @@ class LogWatcher:
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TEAM HUNT PATTERNS
|
||||||
|
# "You received 0.1234 PED from your teammates' activity."
|
||||||
|
PATTERN_TEAM_SHARE_EN = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'You\s+received\s+(\d+(?:\.\d+)?)\s+PED\s+from\s+your\s+teammates',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
PATTERN_TEAM_SHARE_SV = re.compile(
|
||||||
|
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
|
||||||
|
r'Du\s+fick\s+(\d+(?:\.\d+)?)\s+PED\s+från\s+dina\s+lagkamrater',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
EVENT_PATTERNS = {
|
EVENT_PATTERNS = {
|
||||||
'loot_en': PATTERN_LOOT_EN,
|
'loot_en': PATTERN_LOOT_EN,
|
||||||
'loot_sv': PATTERN_LOOT_SV,
|
'loot_sv': PATTERN_LOOT_SV,
|
||||||
|
'loot_no_value_en': PATTERN_LOOT_NO_VALUE_EN,
|
||||||
|
'loot_no_value_sv': PATTERN_LOOT_NO_VALUE_SV,
|
||||||
|
'kill_en': PATTERN_KILL_EN,
|
||||||
|
'kill_sv': PATTERN_KILL_SV,
|
||||||
'global_en': PATTERN_GLOBAL_EN,
|
'global_en': PATTERN_GLOBAL_EN,
|
||||||
'global_sv': PATTERN_GLOBAL_SV,
|
'global_sv': PATTERN_GLOBAL_SV,
|
||||||
'personal_global_en': PATTERN_PERSONAL_GLOBAL_EN,
|
'personal_global_en': PATTERN_PERSONAL_GLOBAL_EN,
|
||||||
'personal_global_sv': PATTERN_PERSONAL_GLOBAL_SV,
|
'personal_global_sv': PATTERN_PERSONAL_GLOBAL_SV,
|
||||||
'hof': PATTERN_HOF_MARKER,
|
'hof_en': PATTERN_HOF_EN,
|
||||||
|
'hof_marker': PATTERN_HOF_MARKER,
|
||||||
'skill_en': PATTERN_SKILL_EN,
|
'skill_en': PATTERN_SKILL_EN,
|
||||||
'skill_sv': PATTERN_SKILL_SV,
|
'skill_sv': PATTERN_SKILL_SV,
|
||||||
'damage_dealt_sv': PATTERN_DAMAGE_DEALT_SV,
|
'damage_dealt_sv': PATTERN_DAMAGE_DEALT_SV,
|
||||||
|
|
@ -266,12 +426,18 @@ class LogWatcher:
|
||||||
'heal_en': PATTERN_HEAL_EN,
|
'heal_en': PATTERN_HEAL_EN,
|
||||||
'weapon_tier_sv': PATTERN_WEAPON_TIER_SV,
|
'weapon_tier_sv': PATTERN_WEAPON_TIER_SV,
|
||||||
'weapon_tier_en': PATTERN_WEAPON_TIER_EN,
|
'weapon_tier_en': PATTERN_WEAPON_TIER_EN,
|
||||||
'evade': PATTERN_EVADE,
|
'evade_en': PATTERN_EVADE_EN,
|
||||||
'decay': PATTERN_DECAY,
|
'evade_sv': PATTERN_EVADE_SV,
|
||||||
'enhancer_broken': PATTERN_ENHANCER_BROKEN,
|
'decay_en': PATTERN_DECAY_EN,
|
||||||
'ped_transfer': PATTERN_PED_TRANSFER,
|
'decay_sv': PATTERN_DECAY_SV,
|
||||||
|
'weapon_broken_sv': PATTERN_WEAPON_BROKEN_SV,
|
||||||
|
'enhancer_broken_en': PATTERN_ENHANCER_BROKEN_EN,
|
||||||
|
'ped_transfer_sv': PATTERN_PED_TRANSFER_SV,
|
||||||
|
'ped_transfer_en': PATTERN_PED_TRANSFER_EN,
|
||||||
'attribute_sv': PATTERN_ATTRIBUTE_SV,
|
'attribute_sv': PATTERN_ATTRIBUTE_SV,
|
||||||
'attribute_en': PATTERN_ATTRIBUTE_EN,
|
'attribute_en': PATTERN_ATTRIBUTE_EN,
|
||||||
|
'team_share_en': PATTERN_TEAM_SHARE_EN,
|
||||||
|
'team_share_sv': PATTERN_TEAM_SHARE_SV,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, log_path: Optional[str] = None,
|
def __init__(self, log_path: Optional[str] = None,
|
||||||
|
|
@ -295,7 +461,7 @@ class LogWatcher:
|
||||||
'damage_dealt': [], 'damage_taken': [], 'heal': [],
|
'damage_dealt': [], 'damage_taken': [], 'heal': [],
|
||||||
'weapon_tier': [], 'evade': [], 'decay': [],
|
'weapon_tier': [], 'evade': [], 'decay': [],
|
||||||
'critical_hit': [], 'ped_transfer': [], 'attribute': [],
|
'critical_hit': [], 'ped_transfer': [], 'attribute': [],
|
||||||
'any': [],
|
'kill': [], 'team_share': [], 'any': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
@ -360,6 +526,28 @@ class LogWatcher:
|
||||||
"""Parse EU timestamp format."""
|
"""Parse EU timestamp format."""
|
||||||
return datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
|
return datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
def _is_shrapnel(self, item_name: str) -> bool:
|
||||||
|
"""Check if item is Shrapnel."""
|
||||||
|
return item_name.strip().lower() == 'shrapnel'
|
||||||
|
|
||||||
|
def _is_universal_ammo(self, item_name: str) -> bool:
|
||||||
|
"""Check if item is Universal Ammo."""
|
||||||
|
name = item_name.strip().lower()
|
||||||
|
return name == 'universal ammo' or name == 'universell ammunition'
|
||||||
|
|
||||||
|
def _categorize_loot(self, item_name: str, value_ped: Decimal) -> LootItem:
|
||||||
|
"""Categorize loot item and return LootItem."""
|
||||||
|
is_shrapnel = self._is_shrapnel(item_name)
|
||||||
|
is_ua = self._is_universal_ammo(item_name)
|
||||||
|
|
||||||
|
return LootItem(
|
||||||
|
name=item_name.strip(),
|
||||||
|
quantity=1, # Will be set by caller
|
||||||
|
value_ped=value_ped,
|
||||||
|
is_shrapnel=is_shrapnel,
|
||||||
|
is_universal_ammo=is_ua
|
||||||
|
)
|
||||||
|
|
||||||
def _parse_line(self, line: str) -> Optional[LogEvent]:
|
def _parse_line(self, line: str) -> Optional[LogEvent]:
|
||||||
"""
|
"""
|
||||||
Parse a single log line.
|
Parse a single log line.
|
||||||
|
|
@ -369,7 +557,18 @@ class LogWatcher:
|
||||||
if not line:
|
if not line:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Try each pattern
|
# Try each pattern in priority order
|
||||||
|
|
||||||
|
# KILL - Swedish
|
||||||
|
match = self.PATTERN_KILL_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return self._create_kill_event(match, line, 'swedish')
|
||||||
|
|
||||||
|
# KILL - English
|
||||||
|
match = self.PATTERN_KILL_EN.match(line)
|
||||||
|
if match:
|
||||||
|
return self._create_kill_event(match, line, 'english')
|
||||||
|
|
||||||
# LOOT - Swedish (prioritize based on your game client)
|
# LOOT - Swedish (prioritize based on your game client)
|
||||||
match = self.PATTERN_LOOT_SV.match(line)
|
match = self.PATTERN_LOOT_SV.match(line)
|
||||||
if match:
|
if match:
|
||||||
|
|
@ -380,6 +579,16 @@ class LogWatcher:
|
||||||
if match:
|
if match:
|
||||||
return self._create_loot_event_en(match, line)
|
return self._create_loot_event_en(match, line)
|
||||||
|
|
||||||
|
# LOOT WITHOUT VALUE - Swedish
|
||||||
|
match = self.PATTERN_LOOT_NO_VALUE_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return self._create_loot_event_no_value(match, line, 'swedish')
|
||||||
|
|
||||||
|
# LOOT WITHOUT VALUE - English
|
||||||
|
match = self.PATTERN_LOOT_NO_VALUE_EN.match(line)
|
||||||
|
if match:
|
||||||
|
return self._create_loot_event_no_value(match, line, 'english')
|
||||||
|
|
||||||
# GLOBAL - Swedish
|
# GLOBAL - Swedish
|
||||||
match = self.PATTERN_GLOBAL_SV.match(line)
|
match = self.PATTERN_GLOBAL_SV.match(line)
|
||||||
if match:
|
if match:
|
||||||
|
|
@ -400,7 +609,12 @@ class LogWatcher:
|
||||||
if match:
|
if match:
|
||||||
return self._create_personal_global_event(match, line, 'english')
|
return self._create_personal_global_event(match, line, 'english')
|
||||||
|
|
||||||
# HOF
|
# HOF - English
|
||||||
|
match = self.PATTERN_HOF_EN.match(line)
|
||||||
|
if match:
|
||||||
|
return self._create_hof_event(match, line, 'english')
|
||||||
|
|
||||||
|
# HOF Marker
|
||||||
match = self.PATTERN_HOF_MARKER.match(line)
|
match = self.PATTERN_HOF_MARKER.match(line)
|
||||||
if match:
|
if match:
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
|
|
@ -452,6 +666,20 @@ class LogWatcher:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# LEVEL UP - Swedish
|
||||||
|
match = self.PATTERN_LEVEL_UP_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='level_up',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'new_level': int(match.group(2)),
|
||||||
|
'skill_name': match.group(3).strip(),
|
||||||
|
'language': 'swedish'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# DAMAGE DEALT - Swedish
|
# DAMAGE DEALT - Swedish
|
||||||
match = self.PATTERN_DAMAGE_DEALT_SV.match(line)
|
match = self.PATTERN_DAMAGE_DEALT_SV.match(line)
|
||||||
if match:
|
if match:
|
||||||
|
|
@ -560,28 +788,56 @@ class LogWatcher:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# EVADE/DODGE/MISS
|
# EVADE/DODGE/MISS - English
|
||||||
match = self.PATTERN_EVADE.match(line)
|
match = self.PATTERN_EVADE_EN.match(line)
|
||||||
if match:
|
if match:
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
timestamp=self._parse_timestamp(match.group(1)),
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
event_type='evade',
|
event_type='evade',
|
||||||
raw_line=line,
|
raw_line=line,
|
||||||
data={'type': match.group(2)}
|
data={'type': match.group(2), 'language': 'english'}
|
||||||
)
|
)
|
||||||
|
|
||||||
# DECAY
|
# EVADE/DODGE/MISS - Swedish
|
||||||
match = self.PATTERN_DECAY.match(line)
|
match = self.PATTERN_EVADE_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='evade',
|
||||||
|
raw_line=line,
|
||||||
|
data={'type': match.group(2), 'language': 'swedish'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# DECAY - English
|
||||||
|
match = self.PATTERN_DECAY_EN.match(line)
|
||||||
if match:
|
if match:
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
timestamp=self._parse_timestamp(match.group(1)),
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
event_type='decay',
|
event_type='decay',
|
||||||
raw_line=line,
|
raw_line=line,
|
||||||
data={'item': match.group(2).strip()}
|
data={
|
||||||
|
'item': match.group(2).strip(),
|
||||||
|
'amount_pec': Decimal(match.group(3)),
|
||||||
|
'language': 'english'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# BROKEN ENHANCER
|
# DECAY - Swedish
|
||||||
match = self.PATTERN_ENHANCER_BROKEN.match(line)
|
match = self.PATTERN_DECAY_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='decay',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'item': match.group(2).strip(),
|
||||||
|
'amount_pec': Decimal(match.group(3)),
|
||||||
|
'language': 'swedish'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# BROKEN ENHANCER - English
|
||||||
|
match = self.PATTERN_ENHANCER_BROKEN_EN.match(line)
|
||||||
if match:
|
if match:
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
timestamp=self._parse_timestamp(match.group(1)),
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
|
@ -589,7 +845,8 @@ class LogWatcher:
|
||||||
raw_line=line,
|
raw_line=line,
|
||||||
data={
|
data={
|
||||||
'enhancer_type': match.group(2).strip(),
|
'enhancer_type': match.group(2).strip(),
|
||||||
'weapon': match.group(3).strip()
|
'weapon': match.group(3).strip(),
|
||||||
|
'language': 'english'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -621,37 +878,114 @@ class LogWatcher:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TEAM SHARE - English
|
||||||
|
match = self.PATTERN_TEAM_SHARE_EN.match(line)
|
||||||
|
if match:
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='team_share',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'amount_ped': Decimal(match.group(2)),
|
||||||
|
'language': 'english'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# TEAM SHARE - Swedish
|
||||||
|
match = self.PATTERN_TEAM_SHARE_SV.match(line)
|
||||||
|
if match:
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='team_share',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'amount_ped': Decimal(match.group(2)),
|
||||||
|
'language': 'swedish'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _create_loot_event_sv(self, match: re.Match, line: str) -> LogEvent:
|
def _create_loot_event_sv(self, match: re.Match, line: str) -> LogEvent:
|
||||||
"""Create loot event from Swedish pattern."""
|
"""Create loot event from Swedish pattern."""
|
||||||
|
item_name = match.group(2).strip()
|
||||||
|
quantity = int(match.group(3))
|
||||||
|
value_ped = Decimal(match.group(4))
|
||||||
|
|
||||||
|
loot_item = self._categorize_loot(item_name, value_ped)
|
||||||
|
loot_item.quantity = quantity
|
||||||
|
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
timestamp=self._parse_timestamp(match.group(1)),
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
event_type='loot',
|
event_type='loot',
|
||||||
raw_line=line,
|
raw_line=line,
|
||||||
data={
|
data={
|
||||||
'item_name': match.group(2).strip(),
|
'item_name': loot_item.name,
|
||||||
'quantity': int(match.group(3)),
|
'quantity': loot_item.quantity,
|
||||||
'value_ped': Decimal(match.group(4)),
|
'value_ped': loot_item.value_ped,
|
||||||
|
'is_shrapnel': loot_item.is_shrapnel,
|
||||||
|
'is_universal_ammo': loot_item.is_universal_ammo,
|
||||||
'language': 'swedish'
|
'language': 'swedish'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_loot_event_en(self, match: re.Match, line: str) -> LogEvent:
|
def _create_loot_event_en(self, match: re.Match, line: str) -> LogEvent:
|
||||||
"""Create loot event from English pattern."""
|
"""Create loot event from English pattern."""
|
||||||
value = match.group(4)
|
item_name = match.group(2).strip()
|
||||||
|
quantity = int(match.group(3))
|
||||||
|
value_ped = Decimal(match.group(4)) if match.group(4) else Decimal('0')
|
||||||
|
|
||||||
|
loot_item = self._categorize_loot(item_name, value_ped)
|
||||||
|
loot_item.quantity = quantity
|
||||||
|
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
timestamp=self._parse_timestamp(match.group(1)),
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
event_type='loot',
|
event_type='loot',
|
||||||
raw_line=line,
|
raw_line=line,
|
||||||
data={
|
data={
|
||||||
'item_name': match.group(2).strip(),
|
'item_name': loot_item.name,
|
||||||
'quantity': int(match.group(3)),
|
'quantity': loot_item.quantity,
|
||||||
'value_ped': Decimal(value) if value else Decimal('0'),
|
'value_ped': loot_item.value_ped,
|
||||||
|
'is_shrapnel': loot_item.is_shrapnel,
|
||||||
|
'is_universal_ammo': loot_item.is_universal_ammo,
|
||||||
'language': 'english'
|
'language': 'english'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_loot_event_no_value(self, match: re.Match, line: str, language: str) -> LogEvent:
|
||||||
|
"""Create loot event without value."""
|
||||||
|
item_name = match.group(2).strip()
|
||||||
|
quantity = int(match.group(3))
|
||||||
|
|
||||||
|
loot_item = self._categorize_loot(item_name, Decimal('0'))
|
||||||
|
loot_item.quantity = quantity
|
||||||
|
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='loot',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'item_name': loot_item.name,
|
||||||
|
'quantity': loot_item.quantity,
|
||||||
|
'value_ped': loot_item.value_ped,
|
||||||
|
'is_shrapnel': loot_item.is_shrapnel,
|
||||||
|
'is_universal_ammo': loot_item.is_universal_ammo,
|
||||||
|
'language': language
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_kill_event(self, match: re.Match, line: str, language: str) -> LogEvent:
|
||||||
|
"""Create kill event."""
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='kill',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'creature_name': match.group(2).strip(),
|
||||||
|
'language': language
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _create_global_event_sv(self, match: re.Match, line: str) -> LogEvent:
|
def _create_global_event_sv(self, match: re.Match, line: str) -> LogEvent:
|
||||||
"""Create global event from Swedish pattern."""
|
"""Create global event from Swedish pattern."""
|
||||||
return LogEvent(
|
return LogEvent(
|
||||||
|
|
@ -694,6 +1028,20 @@ class LogWatcher:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_hof_event(self, match: re.Match, line: str, language: str) -> LogEvent:
|
||||||
|
"""Create Hall of Fame event."""
|
||||||
|
return LogEvent(
|
||||||
|
timestamp=self._parse_timestamp(match.group(1)),
|
||||||
|
event_type='hof',
|
||||||
|
raw_line=line,
|
||||||
|
data={
|
||||||
|
'player_name': match.group(2).strip(),
|
||||||
|
'creature': match.group(3).strip(),
|
||||||
|
'value_ped': Decimal(match.group(4)),
|
||||||
|
'language': language
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
# ASYNC POLLING LOOP
|
# ASYNC POLLING LOOP
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
@ -763,6 +1111,219 @@ class LogWatcher:
|
||||||
self._last_file_size = current_size
|
self._last_file_size = current_size
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HUNTING SESSION TRACKER
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class HuntingSessionTracker:
|
||||||
|
"""
|
||||||
|
Tracks hunting session statistics from LogWatcher events.
|
||||||
|
|
||||||
|
This class accumulates all hunting-related data and provides
|
||||||
|
real-time metrics like profit/loss, return percentage, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.stats = HuntingSessionStats()
|
||||||
|
self._session_start: Optional[datetime] = None
|
||||||
|
self._session_active = False
|
||||||
|
|
||||||
|
# Callbacks for real-time updates
|
||||||
|
self._on_stats_update: Optional[Callable] = None
|
||||||
|
|
||||||
|
def start_session(self):
|
||||||
|
"""Start a new hunting session."""
|
||||||
|
self._session_start = datetime.now()
|
||||||
|
self._session_active = True
|
||||||
|
self.stats = HuntingSessionStats()
|
||||||
|
logger.info("Hunting session started")
|
||||||
|
|
||||||
|
def end_session(self) -> HuntingSessionStats:
|
||||||
|
"""End the current hunting session and return final stats."""
|
||||||
|
self._session_active = False
|
||||||
|
logger.info("Hunting session ended")
|
||||||
|
return self.stats
|
||||||
|
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
"""Check if session is active."""
|
||||||
|
return self._session_active
|
||||||
|
|
||||||
|
def set_stats_callback(self, callback: Callable[[HuntingSessionStats], None]):
|
||||||
|
"""Set callback for real-time stats updates."""
|
||||||
|
self._on_stats_update = callback
|
||||||
|
|
||||||
|
def _notify_update(self):
|
||||||
|
"""Notify listeners of stats update."""
|
||||||
|
if self._on_stats_update:
|
||||||
|
try:
|
||||||
|
self._on_stats_update(self.stats)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Stats callback error: {e}")
|
||||||
|
|
||||||
|
def on_loot(self, event: LogEvent):
|
||||||
|
"""Process loot event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = event.data
|
||||||
|
value_ped = data.get('value_ped', Decimal('0.0'))
|
||||||
|
is_shrapnel = data.get('is_shrapnel', False)
|
||||||
|
is_ua = data.get('is_universal_ammo', False)
|
||||||
|
|
||||||
|
self.stats.total_loot_ped += value_ped
|
||||||
|
|
||||||
|
if is_shrapnel:
|
||||||
|
self.stats.total_shrapnel_ped += value_ped
|
||||||
|
elif is_ua:
|
||||||
|
self.stats.total_universal_ammo_ped += value_ped
|
||||||
|
else:
|
||||||
|
self.stats.total_other_loot_ped += value_ped
|
||||||
|
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_kill(self, event: LogEvent):
|
||||||
|
"""Process kill event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.kills += 1
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_damage_dealt(self, event: LogEvent):
|
||||||
|
"""Process damage dealt event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
damage = event.data.get('damage', Decimal('0.0'))
|
||||||
|
self.stats.damage_dealt += damage
|
||||||
|
self.stats.shots_fired += 1 # Each damage event = 1 shot
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_damage_taken(self, event: LogEvent):
|
||||||
|
"""Process damage taken event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
damage = event.data.get('damage', Decimal('0.0'))
|
||||||
|
self.stats.damage_taken += damage
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_heal(self, event: LogEvent):
|
||||||
|
"""Process heal event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
heal_amount = event.data.get('heal_amount', Decimal('0.0'))
|
||||||
|
self.stats.healing_done += heal_amount
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_global(self, event: LogEvent):
|
||||||
|
"""Process global event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.globals_count += 1
|
||||||
|
|
||||||
|
# Store personal global details
|
||||||
|
if event.event_type == 'personal_global':
|
||||||
|
self.stats.personal_globals.append({
|
||||||
|
'timestamp': event.timestamp,
|
||||||
|
'creature': event.data.get('creature', 'Unknown'),
|
||||||
|
'value_ped': event.data.get('value_ped', Decimal('0.0'))
|
||||||
|
})
|
||||||
|
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_hof(self, event: LogEvent):
|
||||||
|
"""Process Hall of Fame event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.hofs_count += 1
|
||||||
|
|
||||||
|
# Store HoF details
|
||||||
|
if 'creature' in event.data:
|
||||||
|
self.stats.personal_globals.append({
|
||||||
|
'timestamp': event.timestamp,
|
||||||
|
'creature': event.data.get('creature', 'Unknown'),
|
||||||
|
'value_ped': event.data.get('value_ped', Decimal('0.0')),
|
||||||
|
'is_hof': True
|
||||||
|
})
|
||||||
|
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def on_decay(self, event: LogEvent):
|
||||||
|
"""Process decay event."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert PEC to PED
|
||||||
|
amount_pec = event.data.get('amount_pec', Decimal('0.0'))
|
||||||
|
amount_ped = amount_pec / Decimal('100')
|
||||||
|
|
||||||
|
self.stats.weapon_cost_ped += amount_ped
|
||||||
|
self.stats.total_cost_ped += amount_ped
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def add_weapon_cost(self, cost_ped: Decimal):
|
||||||
|
"""Manually add weapon cost (for calculated decay)."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.weapon_cost_ped += cost_ped
|
||||||
|
self.stats.total_cost_ped += cost_ped
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def add_armor_cost(self, cost_ped: Decimal):
|
||||||
|
"""Manually add armor cost."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.armor_cost_ped += cost_ped
|
||||||
|
self.stats.total_cost_ped += cost_ped
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def add_healing_cost(self, cost_ped: Decimal):
|
||||||
|
"""Manually add healing cost."""
|
||||||
|
if not self._session_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stats.healing_cost_ped += cost_ped
|
||||||
|
self.stats.total_cost_ped += cost_ped
|
||||||
|
self._notify_update()
|
||||||
|
|
||||||
|
def get_stats(self) -> HuntingSessionStats:
|
||||||
|
"""Get current stats."""
|
||||||
|
return self.stats
|
||||||
|
|
||||||
|
def get_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get session summary as dictionary."""
|
||||||
|
return {
|
||||||
|
'session_active': self._session_active,
|
||||||
|
'session_start': self._session_start.isoformat() if self._session_start else None,
|
||||||
|
'total_loot_ped': float(self.stats.total_loot_ped),
|
||||||
|
'total_shrapnel_ped': float(self.stats.total_shrapnel_ped),
|
||||||
|
'total_universal_ammo_ped': float(self.stats.total_universal_ammo_ped),
|
||||||
|
'total_other_loot_ped': float(self.stats.total_other_loot_ped),
|
||||||
|
'total_cost_ped': float(self.stats.total_cost_ped),
|
||||||
|
'weapon_cost_ped': float(self.stats.weapon_cost_ped),
|
||||||
|
'armor_cost_ped': float(self.stats.armor_cost_ped),
|
||||||
|
'healing_cost_ped': float(self.stats.healing_cost_ped),
|
||||||
|
'net_profit_ped': float(self.stats.net_profit_ped),
|
||||||
|
'return_percentage': float(self.stats.return_percentage),
|
||||||
|
'cost_per_kill': float(self.stats.cost_per_kill),
|
||||||
|
'dpp': float(self.stats.dpp),
|
||||||
|
'damage_dealt': float(self.stats.damage_dealt),
|
||||||
|
'damage_taken': float(self.stats.damage_taken),
|
||||||
|
'healing_done': float(self.stats.healing_done),
|
||||||
|
'shots_fired': self.stats.shots_fired,
|
||||||
|
'kills': self.stats.kills,
|
||||||
|
'globals_count': self.stats.globals_count,
|
||||||
|
'hofs_count': self.stats.hofs_count,
|
||||||
|
'damage_per_kill': float(self.stats.damage_per_kill),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# MOCK MODE SUPPORT
|
# MOCK MODE SUPPORT
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -774,14 +1335,22 @@ class MockLogGenerator:
|
||||||
"2026-02-08 14:23:15 [System] You received Shrapnel x 123 (Value: 1.23 PED)",
|
"2026-02-08 14:23:15 [System] You received Shrapnel x 123 (Value: 1.23 PED)",
|
||||||
"2026-02-08 14:23:45 [System] You gained 0.45 experience in your Rifle skill",
|
"2026-02-08 14:23:45 [System] You gained 0.45 experience in your Rifle skill",
|
||||||
"2026-02-08 14:24:02 [System] Your Omegaton M2100 has decayed 15 PEC",
|
"2026-02-08 14:24:02 [System] Your Omegaton M2100 has decayed 15 PEC",
|
||||||
"2026-02-08 14:25:30 [System] PlayerOne globals in Twin Peaks for 150.00 PED",
|
"2026-02-08 14:25:30 [Globals] PlayerOne globals in Twin Peaks for 150.00 PED",
|
||||||
"2026-02-08 14:26:10 [System] You received Animal Thyroid Oil x 5",
|
"2026-02-08 14:26:10 [System] You received Animal Thyroid Oil x 5 (Value: 2.50 PED)",
|
||||||
|
"2026-02-08 14:26:15 [System] You killed Araneatrox Young",
|
||||||
"2026-02-08 14:27:55 [System] Congratulations! You have advanced to level 45 in Rifle",
|
"2026-02-08 14:27:55 [System] Congratulations! You have advanced to level 45 in Rifle",
|
||||||
"2026-02-08 14:30:00 [System] PlayerTwo is in the Hall of Fame! Loot of 2500.00 PED",
|
"2026-02-08 14:28:30 [Globals] You killed a creature (Cornundacauda) with a value of 75.00 PED",
|
||||||
|
"2026-02-08 14:30:00 [Hall of Fame] PlayerTwo killed a creature (Atrox) for 2500.00 PED",
|
||||||
|
"2026-02-08 14:31:15 [System] You received Universal Ammo x 50 (Value: 0.50 PED)",
|
||||||
|
"2026-02-08 14:32:20 [System] You inflicted 45.5 points of damage",
|
||||||
|
"2026-02-08 14:32:25 [System] You took 12.3 points of damage",
|
||||||
|
"2026-02-08 14:33:00 [System] You healed yourself 25.0 points",
|
||||||
|
"2026-02-08 14:34:10 [System] Critical hit - Additional damage! You inflicted 89.2 points of damage",
|
||||||
# Swedish examples
|
# Swedish examples
|
||||||
"2025-09-23 19:36:43 [System] Du fick Shrapnel x (4627) Värde: 0.4627 PED",
|
"2025-09-23 19:36:43 [System] Du fick Shrapnel x (4627) Värde: 0.4627 PED",
|
||||||
"2025-09-23 19:36:08 [System] Du har fått 0.3238 erfarenhet i din Translocation färdighet",
|
"2025-09-23 19:36:08 [System] Du har fått 0.3238 erfarenhet i din Translocation färdighet",
|
||||||
"2025-09-23 19:36:18 [System] Du orsakade 13.5 poäng skada",
|
"2025-09-23 19:36:18 [System] Du orsakade 13.5 poäng skada",
|
||||||
|
"2025-09-23 19:37:00 [System] Du dödade Araneatrox Young",
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -804,5 +1373,8 @@ class MockLogGenerator:
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'LogWatcher',
|
'LogWatcher',
|
||||||
'LogEvent',
|
'LogEvent',
|
||||||
|
'LootItem',
|
||||||
|
'HuntingSessionStats',
|
||||||
|
'HuntingSessionTracker',
|
||||||
'MockLogGenerator'
|
'MockLogGenerator'
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Entropia Universe Armor Mechanics - Official Guide
|
||||||
|
|
||||||
|
**Source:** "A Most Complete Guide to Armors (2020 Edition)" - PlanetCalypsoForum
|
||||||
|
**Thread:** https://www.planetcalypsoforum.com/forum/index.php?threads/a-most-complete-guide-to-armors-2020-edition.270884/
|
||||||
|
**Date:** 2026-02-09
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loot 2.0 Armor Changes (VU 15.15, June 2017)
|
||||||
|
|
||||||
|
MindArk made significant changes to how armors work:
|
||||||
|
|
||||||
|
### Key Changes:
|
||||||
|
1. **Armor decays significantly less** per point of damage absorbed
|
||||||
|
2. **No minimum decay** based on total protection - always proportional to damage absorbed
|
||||||
|
3. **Armor absorbs ALL incoming damage** - no more 1.0 damage taken on full absorption (shown as "Deflected" message)
|
||||||
|
4. **Armor and platings decay independently** based on actual damage each absorbs, not both decaying as if absorbing full damage
|
||||||
|
5. **Armor decay is now linear** per point of damage absorbed (not increasing cost per damage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Armor Economy (Durability)
|
||||||
|
|
||||||
|
### Base Protection Cost:
|
||||||
|
**20 hp / pec** - This is the standard armor durability baseline.
|
||||||
|
|
||||||
|
### Comparison with Healing Tools:
|
||||||
|
| Protection Method | Economy |
|
||||||
|
|-------------------|---------|
|
||||||
|
| Armor (base) | ~20 hp/pec |
|
||||||
|
| EMT Kit Ek-2600 Improved | 20 hp/pec |
|
||||||
|
| Refurbished H.E.A.R.T Rank VI | 18 hp/pec |
|
||||||
|
| Vivo S10 | 12.31 hp/pec |
|
||||||
|
| Herb Box | 10.06 hp/pec |
|
||||||
|
| EMT Kit Ek-2350 | 4 hp/pec |
|
||||||
|
|
||||||
|
**Conclusion:** Armor is very economical compared to most healing tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Armor Progression Guide
|
||||||
|
|
||||||
|
### 1. First Armor: Graduation Set
|
||||||
|
- Get by graduating with a Mentor
|
||||||
|
- Each planet has unique graduation armor
|
||||||
|
- **Best choice:** Atlas (NI) or Musca Adjusted (Arkadia)
|
||||||
|
|
||||||
|
### 2. Early Game: Before Ghost
|
||||||
|
**Add plates first!** Get 5B plates for your graduation armor.
|
||||||
|
|
||||||
|
Then choose:
|
||||||
|
- **Gremlin** - Most versatile armor
|
||||||
|
- **Nemesis** - Upgradeable to Adjusted Nemesis (robot armor)
|
||||||
|
- **Ghost** - Most protection for the money (recommended)
|
||||||
|
|
||||||
|
### 3. Mid Game: After Ghost+5B
|
||||||
|
Options (price jump!):
|
||||||
|
- **Adjusted Nemesis** (~2.5k PED) - Good for robots
|
||||||
|
- **Lion** (~5-6.5k PED) - Robot Beacon drops
|
||||||
|
- **Adjusted Boar** (~7.5-9.5k PED) - 30 Impact, Dodge buff
|
||||||
|
- **Looted (L) pieces** - Cheap bridge option
|
||||||
|
|
||||||
|
### 4. End Game
|
||||||
|
- Upgraded Angel
|
||||||
|
- Upgraded Gorgon
|
||||||
|
- Modified Viceroy (avatar bound, ~9k PED)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Damage Absorption Mechanics (Post-Loot 2.0)
|
||||||
|
|
||||||
|
### How Protection Works:
|
||||||
|
1. **Plate takes damage first** (if equipped)
|
||||||
|
2. **Armor takes remaining damage**
|
||||||
|
3. **Player takes overflow** (if any)
|
||||||
|
|
||||||
|
### Decay Calculation:
|
||||||
|
```
|
||||||
|
Plate decay = (damage_absorbed_by_plate / total_plate_protection) × plate_decay_rate
|
||||||
|
Armor decay = (damage_absorbed_by_armor / total_armor_protection) × armor_decay_rate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Point:** Plate and armor decay **independently** based on actual damage each absorbs.
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
- Monster hits: 20 Impact damage
|
||||||
|
- Plate protects: 15 Impact
|
||||||
|
- Armor protects: 5 Impact (remaining)
|
||||||
|
- Player takes: 0 damage (deflected)
|
||||||
|
|
||||||
|
**Decay:**
|
||||||
|
- Plate decays for absorbing 15 damage
|
||||||
|
- Armor decays for absorbing 5 damage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Armor Buffs
|
||||||
|
|
||||||
|
Many armor sets offer buffs:
|
||||||
|
- **20% increased Dodge chance**
|
||||||
|
- **10-20% increased Evade chance**
|
||||||
|
- **3% faster reload speed**
|
||||||
|
- **5% Acceleration**
|
||||||
|
- **1-4% Block chance**
|
||||||
|
- Heal over time
|
||||||
|
|
||||||
|
**Block Chance:** Some upgraded plates have Block Chance - when triggered, hit is nullified (no damage, no decay).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Armor Sets Reference
|
||||||
|
|
||||||
|
### Entry Level:
|
||||||
|
- **Gremlin** - Most versatile early armor
|
||||||
|
- **Ghost** - Best protection for price
|
||||||
|
- **Nemesis** - Robot hunting, upgradeable
|
||||||
|
|
||||||
|
### Mid Level:
|
||||||
|
- **Adjusted Nemesis** - Robot specialist
|
||||||
|
- **Lion** - Stab-focused protection
|
||||||
|
- **Adjusted Boar** - 30 Impact, Dodge buff
|
||||||
|
|
||||||
|
### High Level:
|
||||||
|
- **Angel** (upgraded)
|
||||||
|
- **Gorgon** (upgraded)
|
||||||
|
- **Modified Viceroy** (avatar bound)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plating Strategy
|
||||||
|
|
||||||
|
### General Rule:
|
||||||
|
- **5B plates** are the standard early-game addition
|
||||||
|
- Match plates to mob's damage type
|
||||||
|
- All 7 plates should match (don't mix types)
|
||||||
|
|
||||||
|
### Plate Types:
|
||||||
|
- **5B** - General purpose (Impact/Stab/Cut)
|
||||||
|
- **6A** - Cold/Electric protection
|
||||||
|
- **5C** - Burn/Acid protection
|
||||||
|
- Custom plates for specific mobs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes for Lemontropia Suite
|
||||||
|
|
||||||
|
1. **Decay Formula:** Linear per damage point absorbed
|
||||||
|
2. **Plate Priority:** Plates absorb first, then armor
|
||||||
|
3. **Independent Decay:** Each piece decays separately
|
||||||
|
4. **Deflected Message:** Indicates full absorption (0 player damage)
|
||||||
|
5. **Block Chance:** Roll for block before applying damage
|
||||||
|
6. **Protection Stacking:** Plate + Armor protection values add
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Formulas
|
||||||
|
|
||||||
|
### Cost Per Hit:
|
||||||
|
```
|
||||||
|
armor_cost = (damage_absorbed_by_armor / armor_protection) × armor_decay_per_pec
|
||||||
|
plate_cost = (damage_absorbed_by_plate / plate_protection) × plate_decay_per_pec
|
||||||
|
total_cost = armor_cost + plate_cost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protection Calculation:
|
||||||
|
```
|
||||||
|
total_protection = armor_protection + plate_protection
|
||||||
|
if total_protection >= damage:
|
||||||
|
player_damage = 0
|
||||||
|
absorbed_by_plate = min(damage, plate_protection)
|
||||||
|
absorbed_by_armor = damage - absorbed_by_plate
|
||||||
|
else:
|
||||||
|
player_damage = damage - total_protection
|
||||||
|
absorbed_by_plate = plate_protection
|
||||||
|
absorbed_by_armor = armor_protection
|
||||||
|
```
|
||||||
|
|
@ -13,7 +13,7 @@ from PyQt6.QtCore import Qt, pyqtSignal, QThread
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
|
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats, MedicalTool
|
||||||
|
|
||||||
|
|
||||||
class WeaponLoaderThread(QThread):
|
class WeaponLoaderThread(QThread):
|
||||||
|
|
@ -61,6 +61,21 @@ class FinderLoaderThread(QThread):
|
||||||
self.error_occurred.emit(str(e))
|
self.error_occurred.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class MedicalToolLoaderThread(QThread):
|
||||||
|
"""Thread to load medical tools (FAPs) from API."""
|
||||||
|
|
||||||
|
medical_tools_loaded = pyqtSignal(list)
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
api = EntropiaNexusAPI()
|
||||||
|
tools = api.get_all_medical_tools()
|
||||||
|
self.medical_tools_loaded.emit(tools)
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
class GearSelectorDialog(QDialog):
|
class GearSelectorDialog(QDialog):
|
||||||
"""Dialog for selecting gear from Entropia Nexus."""
|
"""Dialog for selecting gear from Entropia Nexus."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,9 @@ class MainWindow(QMainWindow):
|
||||||
self._selected_armor_stats: Optional[dict] = None
|
self._selected_armor_stats: Optional[dict] = None
|
||||||
self._selected_finder: Optional[str] = None
|
self._selected_finder: Optional[str] = None
|
||||||
self._selected_finder_stats: Optional[dict] = None
|
self._selected_finder_stats: Optional[dict] = None
|
||||||
|
self._selected_medical_tool: Optional[str] = None
|
||||||
|
self._selected_medical_tool_stats: Optional[dict] = None
|
||||||
|
self._selected_loadout: Optional[Any] = None
|
||||||
|
|
||||||
# Setup UI
|
# Setup UI
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
@ -948,6 +951,34 @@ class MainWindow(QMainWindow):
|
||||||
from core.project_manager import LootEvent
|
from core.project_manager import LootEvent
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
def on_heal(event):
|
||||||
|
"""Handle heal events from chat.log.
|
||||||
|
|
||||||
|
Pattern: "You healed yourself X points"
|
||||||
|
Calculates healing cost based on FAP decay and updates HUD.
|
||||||
|
"""
|
||||||
|
heal_amount = event.data.get('heal_amount', Decimal('0'))
|
||||||
|
|
||||||
|
# Calculate heal cost based on selected medical tool decay
|
||||||
|
# Get decay per heal from loadout or use default
|
||||||
|
decay_cost = Decimal('0')
|
||||||
|
if self._selected_loadout and hasattr(self._selected_loadout, 'heal_cost_pec'):
|
||||||
|
# heal_cost_pec is the decay per heal in PEC
|
||||||
|
# Convert to PED for cost calculation
|
||||||
|
decay_cost = self._selected_loadout.heal_cost_pec / Decimal('100')
|
||||||
|
elif self._selected_medical_tool_stats and 'decay' in self._selected_medical_tool_stats:
|
||||||
|
decay_pec = Decimal(str(self._selected_medical_tool_stats['decay']))
|
||||||
|
decay_cost = decay_pec / Decimal('100')
|
||||||
|
else:
|
||||||
|
# Default estimate: 2 PEC per heal
|
||||||
|
decay_cost = Decimal('0.02')
|
||||||
|
|
||||||
|
# Update HUD with heal event
|
||||||
|
self.hud.on_heal_event(heal_amount, decay_cost)
|
||||||
|
|
||||||
|
# Log to UI
|
||||||
|
self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)")
|
||||||
|
|
||||||
def on_loot(event):
|
def on_loot(event):
|
||||||
"""Handle loot events."""
|
"""Handle loot events."""
|
||||||
item_name = event.data.get('item_name', 'Unknown')
|
item_name = event.data.get('item_name', 'Unknown')
|
||||||
|
|
@ -1059,6 +1090,7 @@ class MainWindow(QMainWindow):
|
||||||
self.log_watcher.subscribe('critical_hit', on_critical_hit)
|
self.log_watcher.subscribe('critical_hit', on_critical_hit)
|
||||||
self.log_watcher.subscribe('damage_taken', on_damage_taken)
|
self.log_watcher.subscribe('damage_taken', on_damage_taken)
|
||||||
self.log_watcher.subscribe('evade', on_evade)
|
self.log_watcher.subscribe('evade', on_evade)
|
||||||
|
self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking
|
||||||
|
|
||||||
def _start_log_watcher(self):
|
def _start_log_watcher(self):
|
||||||
"""Start LogWatcher in background thread."""
|
"""Start LogWatcher in background thread."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue