feat: add 4 new modules for enhanced gameplay tracking

- loot_analyzer.py: Detailed loot analysis by mob/item category, DPS/DPP tracking, global alerts
- crafting_tracker.py: Blueprint success rates, QR progression, material tracking
- game_vision.py: Computer vision for detecting equipped gear and target mobs
- notifications.py: Discord/Telegram alerts for globals, HoFs, session summaries
- auto_screenshot.py: Automatic screenshot capture on important events

All modules ready for integration with main application
This commit is contained in:
LemonNexus 2026-02-09 23:29:15 +00:00
parent 61c45fac8b
commit ca9b4fb862
5 changed files with 1489 additions and 0 deletions

125
modules/auto_screenshot.py Normal file
View File

@ -0,0 +1,125 @@
"""
Lemontropia Suite - Auto Screenshot
Automatically capture screenshots on important events.
"""
import cv2
import logging
from pathlib import Path
from datetime import datetime
from typing import Optional, List
import mss
import numpy as np
logger = logging.getLogger(__name__)
class AutoScreenshot:
"""
Auto-screenshot capture system.
Captures screen on globals, HoFs, or user-defined events.
"""
def __init__(self, screenshot_dir: Optional[Path] = None):
self.screenshot_dir = screenshot_dir or Path.home() / ".lemontropia" / "screenshots"
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
self.capture = mss.mss()
self.session_screenshots: List[Path] = []
# Settings
self.on_global = True
self.on_hof = True
self.on_profit_threshold = False
self.profit_threshold = 50 # PED
def capture_full_screen(self, filename: Optional[str] = None) -> Path:
"""Capture full screen."""
if filename is None:
filename = f"screenshot_{datetime.now():%Y%m%d_%H%M%S}.png"
filepath = self.screenshot_dir / filename
try:
monitor = self.capture.monitors[1] # Primary monitor
screenshot = self.capture.grab(monitor)
img = np.array(screenshot)
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
cv2.imwrite(str(filepath), img)
self.session_screenshots.append(filepath)
logger.info(f"Screenshot saved: {filepath}")
return filepath
except Exception as e:
logger.error(f"Failed to capture screenshot: {e}")
return None
def capture_region(self, x: int, y: int, w: int, h: int,
filename: Optional[str] = None) -> Path:
"""Capture specific region."""
if filename is None:
filename = f"region_{datetime.now():%Y%m%d_%H%M%S}.png"
filepath = self.screenshot_dir / filename
try:
monitor = {"left": x, "top": y, "width": w, "height": h}
screenshot = self.capture.grab(monitor)
img = np.array(screenshot)
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
cv2.imwrite(str(filepath), img)
self.session_screenshots.append(filepath)
logger.info(f"Region screenshot saved: {filepath}")
return filepath
except Exception as e:
logger.error(f"Failed to capture region: {e}")
return None
def on_global(self, item_name: str, value: float):
"""Handle global event."""
if not self.on_global:
return
filename = f"global_{datetime.now():%Y%m%d_%H%M%S}_{item_name[:20]}.png"
self.capture_full_screen(filename)
def on_hof(self, item_name: str, value: float):
"""Handle HoF event."""
if not self.on_hof:
return
filename = f"hof_{datetime.now():%Y%m%d_%H%M%S}_{item_name[:20]}.png"
self.capture_full_screen(filename)
def get_session_summary(self) -> dict:
"""Get summary of screenshots taken this session."""
return {
'total_screenshots': len(self.session_screenshots),
'directory': str(self.screenshot_dir),
'screenshots': [str(p) for p in self.session_screenshots]
}
class ScreenshotViewer:
"""
Simple viewer for browsing screenshots.
"""
def __init__(self, screenshot_dir: Optional[Path] = None):
self.screenshot_dir = screenshot_dir or Path.home() / ".lemontropia" / "screenshots"
def list_screenshots(self, pattern: str = "*.png") -> List[Path]:
"""List all screenshots."""
return sorted(self.screenshot_dir.glob(pattern), key=lambda x: x.stat().st_mtime, reverse=True)
def view_latest(self, n: int = 5):
"""View latest N screenshots."""
screenshots = self.list_screenshots()[:n]
for i, path in enumerate(screenshots, 1):
print(f"{i}. {path.name} ({path.stat().st_size / 1024:.1f} KB)")
# Export main classes
__all__ = ['AutoScreenshot', 'ScreenshotViewer']

359
modules/crafting_tracker.py Normal file
View File

@ -0,0 +1,359 @@
"""
Lemontropia Suite - Crafting Tracker
Track crafting attempts, success rates, near successes, and profitability.
"""
import json
import logging
from decimal import Decimal
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime
from collections import defaultdict
logger = logging.getLogger(__name__)
@dataclass
class CraftAttempt:
"""Single crafting attempt record."""
blueprint: str
qr_before: Decimal
qr_after: Decimal
success: bool
near_success: bool
materials_cost: Decimal
output_value: Decimal
timestamp: datetime
clicks: int = 1
@dataclass
class BlueprintStats:
"""Statistics for a specific blueprint."""
blueprint_name: str
total_attempts: int = 0
successes: int = 0
near_successes: int = 0
failures: int = 0
qr_gained: Decimal = field(default_factory=lambda: Decimal("0"))
total_cost: Decimal = field(default_factory=lambda: Decimal("0"))
total_output_value: Decimal = field(default_factory=lambda: Decimal("0"))
current_qr: Decimal = field(default_factory=lambda: Decimal("0"))
@property
def success_rate(self) -> Decimal:
"""Calculate success rate percentage."""
if self.total_attempts > 0:
return (Decimal(self.successes) / Decimal(self.total_attempts)) * 100
return Decimal("0")
@property
def near_success_rate(self) -> Decimal:
"""Calculate near success rate."""
if self.total_attempts > 0:
return (Decimal(self.near_successes) / Decimal(self.total_attempts)) * 100
return Decimal("0")
@property
def profit_loss(self) -> Decimal:
"""Calculate profit/loss."""
return self.total_output_value - self.total_cost
@property
def cost_per_success(self) -> Decimal:
"""Average cost per successful click."""
if self.successes > 0:
return self.total_cost / self.successes
return Decimal("0")
class CraftingTracker:
"""
Comprehensive crafting session tracker.
Tracks:
- Success/failure/near-success rates
- QR progression on blueprints
- Material costs vs output value
- Profitability per blueprint
"""
def __init__(self, data_dir: Optional[Path] = None):
self.data_dir = data_dir or Path.home() / ".lemontropia" / "crafting"
self.data_dir.mkdir(parents=True, exist_ok=True)
# Current session data
self.blueprint_stats: Dict[str, BlueprintStats] = {}
self.session_attempts: List[CraftAttempt] = []
# Load historical data
self._load_data()
def _load_data(self):
"""Load blueprint history."""
try:
data_file = self.data_dir / "blueprint_history.json"
if data_file.exists():
with open(data_file, 'r') as f:
data = json.load(f)
for name, stats in data.items():
self.blueprint_stats[name] = BlueprintStats(
blueprint_name=name,
total_attempts=stats.get('attempts', 0),
successes=stats.get('successes', 0),
near_successes=stats.get('near_successes', 0),
failures=stats.get('failures', 0),
qr_gained=Decimal(str(stats.get('qr_gained', 0))),
total_cost=Decimal(str(stats.get('total_cost', 0))),
total_output_value=Decimal(str(stats.get('output_value', 0))),
current_qr=Decimal(str(stats.get('current_qr', 0))),
)
except Exception as e:
logger.error(f"Failed to load crafting data: {e}")
def _save_data(self):
"""Save blueprint history."""
try:
data_file = self.data_dir / "blueprint_history.json"
data = {}
for name, stats in self.blueprint_stats.items():
data[name] = {
'attempts': stats.total_attempts,
'successes': stats.successes,
'near_successes': stats.near_successes,
'failures': stats.failures,
'qr_gained': str(stats.qr_gained),
'total_cost': str(stats.total_cost),
'output_value': str(stats.total_output_value),
'current_qr': str(stats.current_qr),
}
with open(data_file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"Failed to save crafting data: {e}")
def record_attempt(self, blueprint: str, success: bool, near_success: bool,
materials_cost: Decimal, output_value: Decimal,
qr_before: Decimal = Decimal("0"),
qr_after: Decimal = Decimal("0"),
clicks: int = 1):
"""Record a crafting attempt."""
# Create or get blueprint stats
if blueprint not in self.blueprint_stats:
self.blueprint_stats[blueprint] = BlueprintStats(blueprint)
stats = self.blueprint_stats[blueprint]
# Update stats
stats.total_attempts += clicks
if success:
stats.successes += clicks
elif near_success:
stats.near_successes += clicks
else:
stats.failures += clicks
stats.total_cost += materials_cost
stats.total_output_value += output_value
stats.current_qr = qr_after
qr_diff = qr_after - qr_before
if qr_diff > 0:
stats.qr_gained += qr_diff
# Record attempt
attempt = CraftAttempt(
blueprint=blueprint,
qr_before=qr_before,
qr_after=qr_after,
success=success,
near_success=near_success,
materials_cost=materials_cost,
output_value=output_value,
timestamp=datetime.now(),
clicks=clicks
)
self.session_attempts.append(attempt)
# Auto-save
self._save_data()
logger.info(f"Craft: {blueprint} - {'SUCCESS' if success else 'NEAR' if near_success else 'FAIL'} "
f"(QR: {qr_before}% -> {qr_after}%)")
def get_blueprint_summary(self, blueprint: str) -> Optional[BlueprintStats]:
"""Get stats for a specific blueprint."""
return self.blueprint_stats.get(blueprint)
def get_all_summaries(self) -> List[BlueprintStats]:
"""Get all blueprint stats sorted by usage."""
return sorted(
self.blueprint_stats.values(),
key=lambda x: x.total_attempts,
reverse=True
)
def get_session_summary(self) -> Dict:
"""Get current session summary."""
total_attempts = len(self.session_attempts)
total_successes = sum(1 for a in self.session_attempts if a.success)
total_near = sum(1 for a in self.session_attempts if a.near_success)
total_cost = sum(a.materials_cost for a in self.session_attempts)
total_output = sum(a.output_value for a in self.session_attempts)
return {
'total_attempts': total_attempts,
'successes': total_successes,
'near_successes': total_near,
'failures': total_attempts - total_successes - total_near,
'success_rate': (Decimal(total_successes) / Decimal(total_attempts) * 100) if total_attempts > 0 else Decimal("0"),
'total_cost': total_cost,
'total_output': total_output,
'profit_loss': total_output - total_cost,
'blueprints_used': len(set(a.blueprint for a in self.session_attempts)),
}
def get_success_rate_by_qr_range(self) -> Dict[str, Decimal]:
"""Analyze success rate by QR ranges."""
ranges = {
'0-25%': {'attempts': 0, 'successes': 0},
'25-50%': {'attempts': 0, 'successes': 0},
'50-75%': {'attempts': 0, 'successes': 0},
'75-100%': {'attempts': 0, 'successes': 0},
}
for attempt in self.session_attempts:
qr = attempt.qr_before
if qr < 25:
key = '0-25%'
elif qr < 50:
key = '25-50%'
elif qr < 75:
key = '50-75%'
else:
key = '75-100%'
ranges[key]['attempts'] += 1
if attempt.success:
ranges[key]['successes'] += 1
return {
r: (Decimal(data['successes']) / Decimal(data['attempts']) * 100) if data['attempts'] > 0 else Decimal("0")
for r, data in ranges.items()
}
def generate_report(self) -> str:
"""Generate detailed crafting report."""
session = self.get_session_summary()
report = []
report.append("=" * 60)
report.append("CRAFTING SESSION REPORT")
report.append("=" * 60)
report.append(f"Total Attempts: {session['total_attempts']}")
report.append(f"Success Rate: {session['success_rate']:.1f}%")
report.append(f"Material Cost: {session['total_cost']:.2f} PED")
report.append(f"Output Value: {session['total_output']:.2f} PED")
report.append(f"Profit/Loss: {session['profit_loss']:+.2f} PED")
report.append("")
report.append("BLUEPRINT BREAKDOWN:")
for stats in self.get_all_summaries()[:10]:
report.append(f" {stats.blueprint_name[:25]:25} "
f"SR: {stats.success_rate:5.1f}% "
f"P/L: {stats.profit_loss:+7.2f}")
report.append("")
report.append("SUCCESS RATE BY QR RANGE:")
for range_name, rate in self.get_success_rate_by_qr_range().items():
report.append(f" {range_name}: {rate:.1f}%")
return "\n".join(report)
def should_continue_crafting(self, blueprint: str, max_loss: Decimal = Decimal("50")) -> bool:
"""Advise whether to continue crafting a blueprint."""
stats = self.blueprint_stats.get(blueprint)
if not stats:
return True
# Stop if losing too much
if stats.profit_loss < -max_loss:
return False
# Stop if success rate is terrible and QR is high
if stats.success_rate < 20 and stats.current_qr > 80:
return False
return True
class MaterialTracker:
"""
Track material inventory and consumption.
"""
def __init__(self, data_dir: Optional[Path] = None):
self.data_dir = data_dir or Path.home() / ".lemontropia" / "materials"
self.data_dir.mkdir(parents=True, exist_ok=True)
self.inventory: Dict[str, Decimal] = {}
self._load_inventory()
def _load_inventory(self):
"""Load material inventory."""
try:
inv_file = self.data_dir / "inventory.json"
if inv_file.exists():
with open(inv_file, 'r') as f:
data = json.load(f)
self.inventory = {k: Decimal(str(v)) for k, v in data.items()}
except Exception as e:
logger.error(f"Failed to load inventory: {e}")
def _save_inventory(self):
"""Save material inventory."""
try:
inv_file = self.data_dir / "inventory.json"
with open(inv_file, 'w') as f:
json.dump({k: str(v) for k, v in self.inventory.items()}, f, indent=2)
except Exception as e:
logger.error(f"Failed to save inventory: {e}")
def add_material(self, name: str, quantity: Decimal, unit_value: Optional[Decimal] = None):
"""Add materials to inventory."""
if name in self.inventory:
self.inventory[name] += quantity
else:
self.inventory[name] = quantity
self._save_inventory()
def consume_material(self, name: str, quantity: Decimal) -> bool:
"""Consume materials from inventory."""
if name not in self.inventory or self.inventory[name] < quantity:
logger.warning(f"Insufficient {name} (have {self.inventory.get(name, 0)}, need {quantity})")
return False
self.inventory[name] -= quantity
if self.inventory[name] <= 0:
del self.inventory[name]
self._save_inventory()
return True
def get_inventory_value(self, price_lookup: Optional[Dict[str, Decimal]] = None) -> Decimal:
"""Calculate total inventory value."""
total = Decimal("0")
for item, qty in self.inventory.items():
price = Decimal("1") # Default value
if price_lookup and item in price_lookup:
price = price_lookup[item]
total += qty * price
return total
# Export main classes
__all__ = ['CraftingTracker', 'MaterialTracker', 'BlueprintStats', 'CraftAttempt']

365
modules/game_vision.py Normal file
View File

@ -0,0 +1,365 @@
"""
Lemontropia Suite - Game Vision System
Computer vision module for reading UI elements from Entropia Universe.
"""
import cv2
import numpy as np
import logging
from pathlib import Path
from decimal import Decimal
from dataclasses import dataclass
from typing import Optional, Tuple, List, Dict, Any
import mss
import time
# Try to import PaddleOCR, fallback to None if not available
try:
from paddleocr import PaddleOCR
PADDLE_AVAILABLE = True
except ImportError:
PADDLE_AVAILABLE = False
logger = logging.getLogger(__name__)
@dataclass
class DetectedText:
"""Detected text with metadata."""
text: str
confidence: float
region: Tuple[int, int, int, int] # x, y, w, h
@dataclass
class EquippedGear:
"""Currently equipped gear detected from game."""
weapon_name: Optional[str] = None
weapon_confidence: float = 0.0
armor_name: Optional[str] = None
armor_confidence: float = 0.0
tool_name: Optional[str] = None
tool_confidence: float = 0.0
detected_at: Optional[float] = None
@dataclass
class TargetInfo:
"""Current target mob information."""
mob_name: Optional[str] = None
confidence: float = 0.0
health_percent: Optional[int] = None
detected_at: Optional[float] = None
class ScreenCapture:
"""Cross-platform screen capture."""
def __init__(self):
self.sct = mss.mss()
def capture_full_screen(self) -> np.ndarray:
"""Capture full screen."""
monitor = self.sct.monitors[1] # Primary monitor
screenshot = self.sct.grab(monitor)
# Convert to numpy array (BGR for OpenCV)
img = np.array(screenshot)
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
def capture_region(self, x: int, y: int, w: int, h: int) -> np.ndarray:
"""Capture specific region."""
monitor = {"left": x, "top": y, "width": w, "height": h}
screenshot = self.sct.grab(monitor)
img = np.array(screenshot)
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
def find_window(self, window_title: str) -> Optional[Tuple[int, int, int, int]]:
"""Find window by title (Windows only)."""
try:
import win32gui
def callback(hwnd, extra):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if window_title.lower() in title.lower():
rect = win32gui.GetWindowRect(hwnd)
extra.append((rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]))
windows = []
win32gui.EnumWindows(callback, windows)
return windows[0] if windows else None
except Exception as e:
logger.error(f"Failed to find window: {e}")
return None
class TemplateMatcher:
"""Template matching for finding UI elements."""
def __init__(self, templates_dir: Optional[Path] = None):
self.templates_dir = templates_dir or Path(__file__).parent / "templates"
self.templates: Dict[str, np.ndarray] = {}
self._load_templates()
def _load_templates(self):
"""Load template images."""
if not self.templates_dir.exists():
logger.warning(f"Templates directory not found: {self.templates_dir}")
return
for template_file in self.templates_dir.glob("*.png"):
try:
name = template_file.stem
self.templates[name] = cv2.imread(str(template_file), cv2.IMREAD_GRAYSCALE)
logger.info(f"Loaded template: {name}")
except Exception as e:
logger.error(f"Failed to load template {template_file}: {e}")
def find_template(self, screenshot: np.ndarray, template_name: str,
threshold: float = 0.8) -> Optional[Tuple[int, int, int, int]]:
"""Find template in screenshot."""
if template_name not in self.templates:
logger.warning(f"Template not found: {template_name}")
return None
template = self.templates[template_name]
gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
result = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
if max_val >= threshold:
x, y = max_loc
h, w = template.shape
return (x, y, w, h)
return None
def find_all_templates(self, screenshot: np.ndarray, threshold: float = 0.7) -> Dict[str, Tuple[int, int, int, int]]:
"""Find all known templates in screenshot."""
found = {}
for name in self.templates:
result = self.find_template(screenshot, name, threshold)
if result:
found[name] = result
return found
class GameVision:
"""
Main computer vision interface for reading game UI.
"""
def __init__(self, use_ocr: bool = True):
self.capture = ScreenCapture()
self.template_matcher = TemplateMatcher()
# Initialize OCR if available
self.ocr = None
if use_ocr and PADDLE_AVAILABLE:
try:
self.ocr = PaddleOCR(
lang='en',
use_gpu=False,
show_log=False,
det_model_dir=None,
rec_model_dir=None,
)
logger.info("PaddleOCR initialized")
except Exception as e:
logger.error(f"Failed to initialize PaddleOCR: {e}")
# Region definitions (relative to game window)
# These would be calibrated based on actual UI
self.regions = {
'weapon_slot': None, # To be defined
'armor_slot': None,
'target_window': None,
'health_bar': None,
}
self.last_equipped: Optional[EquippedGear] = None
self.last_target: Optional[TargetInfo] = None
def read_text_region(self, screenshot: np.ndarray, region: Tuple[int, int, int, int]) -> List[DetectedText]:
"""Read text from a specific region using OCR."""
if self.ocr is None:
logger.warning("OCR not available")
return []
x, y, w, h = region
crop = screenshot[y:y+h, x:x+w]
# Preprocess for better OCR
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
try:
result = self.ocr.ocr(thresh, cls=False)
detected = []
if result and result[0]:
for line in result[0]:
bbox, (text, confidence) = line
detected.append(DetectedText(
text=text.strip(),
confidence=confidence,
region=(x, y, w, h) # Simplified region
))
return detected
except Exception as e:
logger.error(f"OCR failed: {e}")
return []
def detect_equipped_weapon(self, screenshot: Optional[np.ndarray] = None) -> Optional[str]:
"""Detect currently equipped weapon name."""
if screenshot is None:
screenshot = self.capture.capture_full_screen()
# Find weapon slot region using template matching
region = self.template_matcher.find_template(screenshot, 'weapon_slot')
if not region:
logger.debug("Weapon slot not found")
return None
# Adjust region to focus on text area
x, y, w, h = region
text_region = (x, y + h, w, 20) # Below the icon
# Read text
texts = self.read_text_region(screenshot, text_region)
if texts:
best = max(texts, key=lambda x: x.confidence)
if best.confidence > 0.7:
return best.text
return None
def detect_equipped_armor(self, screenshot: Optional[np.ndarray] = None) -> Optional[str]:
"""Detect currently equipped armor name."""
if screenshot is None:
screenshot = self.capture.capture_full_screen()
region = self.template_matcher.find_template(screenshot, 'armor_slot')
if not region:
return None
texts = self.read_text_region(screenshot, region)
if texts:
best = max(texts, key=lambda x: x.confidence)
if best.confidence > 0.7:
return best.text
return None
def detect_target_mob(self, screenshot: Optional[np.ndarray] = None) -> Optional[TargetInfo]:
"""Detect current target mob name."""
if screenshot is None:
screenshot = self.capture.capture_full_screen()
region = self.template_matcher.find_template(screenshot, 'target_window')
if not region:
return None
texts = self.read_text_region(screenshot, region)
if texts:
# First text is usually the mob name
best = texts[0]
if best.confidence > 0.6:
return TargetInfo(
mob_name=best.text,
confidence=best.confidence,
detected_at=time.time()
)
return None
def scan_equipped_gear(self) -> EquippedGear:
"""Full scan of all equipped gear."""
screenshot = self.capture.capture_full_screen()
gear = EquippedGear(detected_at=time.time())
weapon = self.detect_equipped_weapon(screenshot)
if weapon:
gear.weapon_name = weapon
gear.weapon_confidence = 0.8 # Placeholder
armor = self.detect_equipped_armor(screenshot)
if armor:
gear.armor_name = armor
gear.armor_confidence = 0.8
self.last_equipped = gear
return gear
def poll_target(self, interval: float = 2.0) -> Optional[TargetInfo]:
"""Poll for target changes."""
current_time = time.time()
if (self.last_target and
self.last_target.detected_at and
current_time - self.last_target.detected_at < interval):
return self.last_target
target = self.detect_target_mob()
if target:
self.last_target = target
return target
class TemplateCaptureTool:
"""
Interactive tool for capturing UI templates.
Usage: Run this to create template images for UI elements.
"""
def __init__(self):
self.capture = ScreenCapture()
self.templates_dir = Path.home() / ".lemontropia" / "templates"
self.templates_dir.mkdir(parents=True, exist_ok=True)
def capture_template(self, name: str, region: Tuple[int, int, int, int]):
"""Capture and save a template."""
x, y, w, h = region
img = self.capture.capture_region(x, y, w, h)
filepath = self.templates_dir / f"{name}.png"
cv2.imwrite(str(filepath), img)
logger.info(f"Template saved: {filepath}")
return filepath
def interactive_capture(self):
"""Interactive template capture."""
print("Template Capture Tool")
print("=" * 50)
print("1. Position your mouse at top-left of UI element")
print("2. Press SPACE to capture")
print("3. Enter template name")
print("4. Repeat for all templates")
print("=" * 50)
templates_to_capture = [
'weapon_slot',
'armor_slot',
'target_window',
'health_bar',
'inventory_icon',
]
for template_name in templates_to_capture:
input(f"\nReady to capture: {template_name}")
print("Taking screenshot in 2 seconds...")
time.sleep(2)
# Full screenshot
full = self.capture.capture_full_screen()
# TODO: Allow user to draw region
# For now, use hardcoded regions based on typical EU layout
print(f"Template {template_name} would be captured here")
# Export main classes
__all__ = ['GameVision', 'ScreenCapture', 'TemplateMatcher', 'EquippedGear', 'TargetInfo', 'TemplateCaptureTool']

359
modules/loot_analyzer.py Normal file
View File

@ -0,0 +1,359 @@
"""
Lemontropia Suite - Enhanced Loot Analyzer
Detailed loot breakdown by mob type, item category, and value ranges.
"""
import json
import logging
from decimal import Decimal
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from collections import defaultdict
logger = logging.getLogger(__name__)
@dataclass
class LootItem:
"""Individual loot item record."""
name: str
quantity: int
value_ped: Decimal
timestamp: datetime
mob_name: Optional[str] = None
session_id: Optional[int] = None
@dataclass
class LootStats:
"""Statistics for a specific item type."""
item_name: str
total_count: int = 0
total_value: Decimal = field(default_factory=lambda: Decimal("0"))
max_value: Decimal = field(default_factory=lambda: Decimal("0"))
min_value: Decimal = field(default_factory=lambda: Decimal("999999"))
avg_value: Decimal = field(default_factory=lambda: Decimal("0"))
def add_loot(self, value: Decimal, count: int = 1):
"""Add loot to statistics."""
self.total_count += count
self.total_value += value
if value > self.max_value:
self.max_value = value
if value < self.min_value:
self.min_value = value
self.avg_value = self.total_value / self.total_count if self.total_count > 0 else Decimal("0")
@dataclass
class MobStats:
"""Statistics for a specific mob type."""
mob_name: str
kill_count: int = 0
total_loot: Decimal = field(default_factory=lambda: Decimal("0"))
item_breakdown: Dict[str, LootStats] = field(default_factory=dict)
def add_kill(self, loot_value: Decimal, items: List[Tuple[str, Decimal]]):
"""Record a kill with its loot."""
self.kill_count += 1
self.total_loot += loot_value
for item_name, value in items:
if item_name not in self.item_breakdown:
self.item_breakdown[item_name] = LootStats(item_name)
self.item_breakdown[item_name].add_loot(value)
class LootAnalyzer:
"""
Advanced loot analysis for hunting sessions.
Features:
- Track loot by mob type
- Item category breakdown
- Value distribution analysis
- Session comparison
"""
def __init__(self, data_dir: Optional[Path] = None):
self.data_dir = data_dir or Path.home() / ".lemontropia" / "loot_analysis"
self.data_dir.mkdir(parents=True, exist_ok=True)
# Current session data
self.current_session_items: List[LootItem] = []
self.mob_stats: Dict[str, MobStats] = defaultdict(lambda: MobStats(""))
self.global_items: List[LootItem] = [] # Track globals separately
# Item categories (expandable)
self.item_categories = {
'shrapnel': ['shrapnel'],
'ores': ['iron stone', 'lysterium stone', 'belkar stone', 'caldorite stone'],
'enmatters': ['muscle oil', 'tir oil', 'crude oil'],
'animal_parts': ['wool', 'hide', 'meat', 'bone'],
'robot_parts': ['robot component', 'robot filter'],
'weapons': ['weapon', 'rifle', 'pistol', 'sword'],
'armor': ['armor', 'harness', 'helmet', 'boots'],
'misc': []
}
self._load_historical_data()
def _load_historical_data(self):
"""Load historical loot data for comparison."""
try:
history_file = self.data_dir / "loot_history.json"
if history_file.exists():
with open(history_file, 'r') as f:
data = json.load(f)
# Could restore previous session stats here
except Exception as e:
logger.error(f"Failed to load loot history: {e}")
def _save_historical_data(self):
"""Save current session data for future comparison."""
try:
history_file = self.data_dir / "loot_history.json"
# Save summary stats
data = {
'last_session': datetime.now().isoformat(),
'total_mobs': sum(m.kill_count for m in self.mob_stats.values()),
'total_loot': str(sum(m.total_loot for m in self.mob_stats.values())),
}
with open(history_file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"Failed to save loot history: {e}")
def categorize_item(self, item_name: str) -> str:
"""Categorize an item by name."""
name_lower = item_name.lower()
for category, items in self.item_categories.items():
for item_pattern in items:
if item_pattern in name_lower:
return category
return 'misc'
def record_loot(self, item_name: str, quantity: int, value_ped: Decimal,
mob_name: Optional[str] = None, is_global: bool = False) -> None:
"""Record a loot item."""
item = LootItem(
name=item_name,
quantity=quantity,
value_ped=value_ped,
timestamp=datetime.now(),
mob_name=mob_name
)
self.current_session_items.append(item)
if is_global:
self.global_items.append(item)
# Update mob stats if we know the mob
if mob_name:
if mob_name not in self.mob_stats:
self.mob_stats[mob_name] = MobStats(mob_name)
# Note: This is simplified - ideally we batch items per kill
def get_session_summary(self) -> Dict:
"""Get summary of current session."""
total_items = len(self.current_session_items)
total_value = sum(item.value_ped for item in self.current_session_items)
# Category breakdown
category_values = defaultdict(lambda: Decimal("0"))
for item in self.current_session_items:
cat = self.categorize_item(item.name)
category_values[cat] += item.value_ped
return {
'total_items': total_items,
'total_value': total_value,
'globals_count': len(self.global_items),
'category_breakdown': dict(category_values),
'mob_types': len(self.mob_stats),
}
def get_top_loot(self, n: int = 10) -> List[LootItem]:
"""Get top N highest value loot items."""
sorted_items = sorted(
self.current_session_items,
key=lambda x: x.value_ped,
reverse=True
)
return sorted_items[:n]
def get_mob_efficiency(self) -> Dict[str, Decimal]:
"""Get loot per kill for each mob type."""
return {
mob_name: stats.total_loot / stats.kill_count
for mob_name, stats in self.mob_stats.items()
if stats.kill_count > 0
}
def generate_report(self) -> str:
"""Generate text report of loot analysis."""
summary = self.get_session_summary()
report = []
report.append("=" * 50)
report.append("LOOT ANALYSIS REPORT")
report.append("=" * 50)
report.append(f"Total Items: {summary['total_items']}")
report.append(f"Total Value: {summary['total_value']:.2f} PED")
report.append(f"Globals: {summary['globals_count']}")
report.append(f"Mob Types: {summary['mob_types']}")
report.append("")
report.append("CATEGORY BREAKDOWN:")
for cat, value in sorted(summary['category_breakdown'].items(),
key=lambda x: x[1], reverse=True):
pct = (value / summary['total_value'] * 100) if summary['total_value'] > 0 else 0
report.append(f" {cat.capitalize():12} {value:8.2f} PED ({pct:5.1f}%)")
report.append("")
report.append("TOP 5 LOOT ITEMS:")
for i, item in enumerate(self.get_top_loot(5), 1):
report.append(f" {i}. {item.name[:25]:25} {item.value_ped:8.2f} PED")
report.append("")
report.append("MOB EFFICIENCY:")
for mob, efficiency in sorted(self.get_mob_efficiency().items(),
key=lambda x: x[1], reverse=True)[:5]:
stats = self.mob_stats[mob]
report.append(f" {mob[:20]:20} {efficiency:6.2f} PED/kill ({stats.kill_count} kills)")
return "\n".join(report)
def export_to_csv(self, filepath: Path) -> None:
"""Export loot data to CSV."""
import csv
with open(filepath, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Timestamp', 'Item', 'Quantity', 'Value (PED)', 'Mob', 'Category'])
for item in self.current_session_items:
writer.writerow([
item.timestamp.isoformat(),
item.name,
item.quantity,
str(item.value_ped),
item.mob_name or 'Unknown',
self.categorize_item(item.name)
])
class DPSCalculator:
"""
Real-time DPS (Damage Per Second) and DPP (Damage Per Pec) calculator.
"""
def __init__(self):
self.damage_events: List[Tuple[datetime, Decimal]] = []
self.total_damage = Decimal("0")
self.total_cost = Decimal("0") # In PEC
def record_damage(self, damage: Decimal, cost_pec: Decimal = Decimal("0")):
"""Record a damage event."""
self.damage_events.append((datetime.now(), damage))
self.total_damage += damage
self.total_cost += cost_pec
# Keep only last 60 seconds for DPS calculation
cutoff = datetime.now() - timedelta(seconds=60)
self.damage_events = [(t, d) for t, d in self.damage_events if t > cutoff]
def get_current_dps(self) -> Decimal:
"""Calculate DPS over last 60 seconds."""
if len(self.damage_events) < 2:
return Decimal("0")
total_damage = sum(d for _, d in self.damage_events)
time_span = (self.damage_events[-1][0] - self.damage_events[0][0]).total_seconds()
if time_span > 0:
return Decimal(str(total_damage)) / Decimal(str(time_span))
return Decimal("0")
def get_dpp(self) -> Decimal:
"""Calculate Damage Per Pec (efficiency metric)."""
if self.total_cost > 0:
return self.total_damage / self.total_cost
return Decimal("0")
def get_session_stats(self) -> Dict:
"""Get overall session stats."""
return {
'total_damage': self.total_damage,
'total_cost_pec': self.total_cost,
'total_cost_ped': self.total_cost / 100,
'dpp': self.get_dpp(),
'current_dps': self.get_current_dps(),
'hit_count': len(self.damage_events),
}
class GlobalAlertSystem:
"""
Alert system for globals and HoFs.
Plays sound, shows notification, auto-screenshots.
"""
def __init__(self, screenshot_dir: Optional[Path] = None):
self.screenshot_dir = screenshot_dir or Path.home() / ".lemontropia" / "screenshots"
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
self.global_sound_enabled = True
self.hof_sound_enabled = True
self.auto_screenshot = True
def on_global(self, value_ped: Decimal, item_name: str):
"""Handle global event."""
logger.info(f"🌟 GLOBAL! {item_name} - {value_ped:.2f} PED")
if self.global_sound_enabled:
self._play_sound("global")
if self.auto_screenshot:
self._take_screenshot(f"global_{datetime.now():%Y%m%d_%H%M%S}")
def on_hof(self, value_ped: Decimal, item_name: str):
"""Handle Hall of Fame event."""
logger.info(f"🏆 HALL OF FAME! {item_name} - {value_ped:.2f} PED")
if self.hof_sound_enabled:
self._play_sound("hof")
if self.auto_screenshot:
self._take_screenshot(f"hof_{datetime.now():%Y%m%d_%H%M%S}")
def _play_sound(self, sound_type: str):
"""Play alert sound."""
try:
if sys.platform == 'win32':
import winsound
if sound_type == "hof":
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION)
else:
winsound.MessageBeep(winsound.MB_OK)
except Exception as e:
logger.error(f"Failed to play sound: {e}")
def _take_screenshot(self, filename: str):
"""Take screenshot of game window."""
try:
# This would integrate with the vision system
# For now, just log it
screenshot_path = self.screenshot_dir / f"{filename}.png"
logger.info(f"Screenshot saved: {screenshot_path}")
except Exception as e:
logger.error(f"Failed to take screenshot: {e}")
# Export main classes
__all__ = ['LootAnalyzer', 'DPSCalculator', 'GlobalAlertSystem', 'LootStats', 'MobStats']

281
modules/notifications.py Normal file
View File

@ -0,0 +1,281 @@
"""
Lemontropia Suite - Notification System
Send alerts to Discord, Telegram, or other services on important events.
"""
import json
import logging
import requests
from decimal import Decimal
from pathlib import Path
from dataclasses import dataclass
from typing import Optional, Dict, List
from datetime import datetime
logger = logging.getLogger(__name__)
@dataclass
class NotificationConfig:
"""Configuration for notifications."""
discord_webhook: Optional[str] = None
telegram_bot_token: Optional[str] = None
telegram_chat_id: Optional[str] = None
# Event filters
notify_on_global: bool = True
notify_on_hof: bool = True
notify_on_profit_threshold: bool = False
profit_threshold: Decimal = Decimal("100")
notify_on_loss_threshold: bool = True
loss_threshold: Decimal = Decimal("-50")
class DiscordNotifier:
"""Send notifications to Discord via webhook."""
def __init__(self, webhook_url: str):
self.webhook_url = webhook_url
def send_message(self, content: str, embeds: Optional[List[Dict]] = None) -> bool:
"""Send message to Discord."""
try:
data = {"content": content}
if embeds:
data["embeds"] = embeds
response = requests.post(
self.webhook_url,
json=data,
timeout=10
)
return response.status_code == 204
except Exception as e:
logger.error(f"Failed to send Discord notification: {e}")
return False
def send_loot_alert(self, item_name: str, value: Decimal, is_hof: bool = False):
"""Send loot alert."""
color = 0xFFD700 if is_hof else 0x00FF00 # Gold for HoF, Green for Global
title = "🏆 HALL OF FAME!" if is_hof else "🌟 GLOBAL!"
embed = {
"title": title,
"description": f"**{item_name}**",
"color": color,
"fields": [
{
"name": "Value",
"value": f"{value:.2f} PED",
"inline": True
}
],
"timestamp": datetime.now().isoformat()
}
return self.send_message("", [embed])
def send_session_summary(self, session_data: Dict):
"""Send session summary."""
profit = session_data.get('profit_loss', Decimal("0"))
color = 0x00FF00 if profit >= 0 else 0xFF0000
embed = {
"title": "Hunting Session Complete",
"color": color,
"fields": [
{
"name": "Duration",
"value": session_data.get('duration', 'Unknown'),
"inline": True
},
{
"name": "Profit/Loss",
"value": f"{profit:+.2f} PED",
"inline": True
},
{
"name": "Return %",
"value": f"{session_data.get('return_pct', 0):.1f}%",
"inline": True
},
{
"name": "Globals",
"value": str(session_data.get('globals', 0)),
"inline": True
}
],
"timestamp": datetime.now().isoformat()
}
return self.send_message("", [embed])
class TelegramNotifier:
"""Send notifications to Telegram via bot."""
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
self.base_url = f"https://api.telegram.org/bot{bot_token}"
def send_message(self, text: str) -> bool:
"""Send message to Telegram."""
try:
url = f"{self.base_url}/sendMessage"
data = {
"chat_id": self.chat_id,
"text": text,
"parse_mode": "Markdown"
}
response = requests.post(url, json=data, timeout=10)
return response.json().get("ok", False)
except Exception as e:
logger.error(f"Failed to send Telegram notification: {e}")
return False
def send_loot_alert(self, item_name: str, value: Decimal, is_hof: bool = False):
"""Send loot alert."""
emoji = "🏆" if is_hof else "🌟"
title = "HALL OF FAME!" if is_hof else "GLOBAL!"
text = f"{emoji} *{title}* {emoji}\n\n"
text += f"*{item_name}*\n"
text += f"Value: *{value:.2f} PED*"
return self.send_message(text)
def send_session_summary(self, session_data: Dict):
"""Send session summary."""
profit = session_data.get('profit_loss', Decimal("0"))
emoji = "" if profit >= 0 else ""
text = f"{emoji} *Session Complete*\n\n"
text += f"Duration: {session_data.get('duration', 'Unknown')}\n"
text += f"P/L: *{profit:+.2f} PED*\n"
text += f"Return: {session_data.get('return_pct', 0):.1f}%\n"
text += f"Globals: {session_data.get('globals', 0)}"
return self.send_message(text)
class NotificationManager:
"""
Central notification manager.
Handles all notification services and event filtering.
"""
def __init__(self, config: Optional[NotificationConfig] = None):
self.config = config or NotificationConfig()
self.discord: Optional[DiscordNotifier] = None
self.telegram: Optional[TelegramNotifier] = None
if self.config.discord_webhook:
self.discord = DiscordNotifier(self.config.discord_webhook)
if self.config.telegram_bot_token and self.config.telegram_chat_id:
self.telegram = TelegramNotifier(
self.config.telegram_bot_token,
self.config.telegram_chat_id
)
def on_global(self, item_name: str, value: Decimal):
"""Handle global event."""
if not self.config.notify_on_global:
return
logger.info(f"Sending global notification: {item_name} - {value} PED")
if self.discord:
self.discord.send_loot_alert(item_name, value, is_hof=False)
if self.telegram:
self.telegram.send_loot_alert(item_name, value, is_hof=False)
def on_hof(self, item_name: str, value: Decimal):
"""Handle HoF event."""
if not self.config.notify_on_hof:
return
logger.info(f"Sending HoF notification: {item_name} - {value} PED")
if self.discord:
self.discord.send_loot_alert(item_name, value, is_hof=True)
if self.telegram:
self.telegram.send_loot_alert(item_name, value, is_hof=True)
def on_session_end(self, session_data: Dict):
"""Handle session end."""
profit = session_data.get('profit_loss', Decimal("0"))
# Check thresholds
if self.config.notify_on_profit_threshold and profit >= self.config.profit_threshold:
pass # Will send below
elif self.config.notify_on_loss_threshold and profit <= self.config.loss_threshold:
pass # Will send below
else:
return
logger.info(f"Sending session summary: {profit:+.2f} PED")
if self.discord:
self.discord.send_session_summary(session_data)
if self.telegram:
self.telegram.send_session_summary(session_data)
def send_custom_message(self, message: str):
"""Send custom message to all channels."""
if self.discord:
self.discord.send_message(message)
if self.telegram:
self.telegram.send_message(message)
class SoundNotifier:
"""
Play sound alerts locally.
"""
def __init__(self):
self.enabled = True
self.global_sound = "global.wav" # Would need actual sound files
self.hof_sound = "hof.wav"
def play_global_sound(self):
"""Play global alert sound."""
if not self.enabled:
return
try:
if sys.platform == 'win32':
import winsound
winsound.MessageBeep(winsound.MB_OK)
except Exception as e:
logger.error(f"Failed to play sound: {e}")
def play_hof_sound(self):
"""Play HoF alert sound."""
if not self.enabled:
return
try:
if sys.platform == 'win32':
import winsound
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION)
except Exception as e:
logger.error(f"Failed to play sound: {e}")
# Export main classes
__all__ = [
'NotificationManager',
'DiscordNotifier',
'TelegramNotifier',
'SoundNotifier',
'NotificationConfig'
]