feat: Resizable window, OCR scanners, customizable dashboard

This commit is contained in:
LemonNexus 2026-02-13 17:12:58 +00:00
parent 5b127cf99e
commit 72c3c132ca
5 changed files with 778 additions and 769 deletions

View File

@ -52,19 +52,15 @@ class OverlayWindow(QMainWindow):
self.hide_overlay()
def _setup_window(self):
"""Configure window with EU styling - shows in taskbar."""
"""Configure window - resizable and shows in taskbar."""
self.setWindowTitle("EU-Utility")
# Frameless, but NOT Tool (so it shows in taskbar)
# WindowStaysOnTopHint makes it stay on top without hiding from taskbar
# Resizable window (no FramelessWindowHint), stays on top
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Clean, game-like size
self.setMinimumSize(600, 400)
self.resize(850, 600)
self._center_window()
@ -337,19 +333,22 @@ class OverlayWindow(QMainWindow):
"""
def _load_plugins(self):
"""Load plugins into sidebar and stack."""
for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()):
"""Load plugins into sidebar and stack - FIXED indexing."""
plugins_list = list(self.plugin_manager.get_all_plugins().items())
for idx, (plugin_id, plugin) in enumerate(plugins_list):
# Get icon name
icon_name = get_plugin_icon_name(plugin.name)
# Add to list view
list_item = QListWidgetItem(plugin.name)
list_item.setData(Qt.ItemDataRole.UserRole, idx)
list_item.setData(Qt.ItemDataRole.UserRole, idx) # Store correct index
self.plugin_list.addItem(list_item)
# Add to icon grid
icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
self.icon_grid_layout.addWidget(icon_btn)
self.plugin_buttons.append(icon_btn) # Track buttons
# Add plugin UI to stack
try:

View File

@ -1,230 +1,326 @@
"""
EU-Utility - Dashboard Plugin
EU-Utility - Dashboard Plugin with Customizable Widgets
Main dashboard with customizable widgets and overview.
Customizable start page with avatar statistics.
"""
import json
from pathlib import Path
from datetime import datetime, timedelta
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QGridLayout, QFrame, QScrollArea,
QSizePolicy
QSizePolicy, QCheckBox, QDialog, QListWidget,
QListWidgetItem, QDialogButtonBox
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QColor
from PyQt6.QtGui import QColor, QFont
from core.eu_styles import EU_COLORS, EU_STYLES
from core.eu_styles import EU_COLORS
from plugins.base_plugin import BasePlugin
class DashboardPlugin(BasePlugin):
"""Main dashboard with overview of all features."""
"""Customizable dashboard with avatar statistics."""
name = "Dashboard"
version = "1.0.0"
version = "2.0.0"
author = "ImpulsiveFPS"
description = "Overview dashboard with widgets"
description = "Customizable start page with avatar stats"
hotkey = "ctrl+shift+home"
# Available widgets
AVAILABLE_WIDGETS = {
'ped_balance': {'name': 'PED Balance', 'icon': 'dollar-sign', 'default': True},
'skill_count': {'name': 'Skills Tracked', 'icon': 'trending-up', 'default': True},
'inventory_items': {'name': 'Inventory Items', 'icon': 'archive', 'default': True},
'current_dpp': {'name': 'Current DPP', 'icon': 'crosshair', 'default': True},
'total_gains_today': {'name': "Today's Skill Gains", 'icon': 'zap', 'default': True},
'professions_count': {'name': 'Professions', 'icon': 'award', 'default': False},
'missions_active': {'name': 'Active Missions', 'icon': 'map', 'default': False},
'codex_progress': {'name': 'Codex Progress', 'icon': 'book', 'default': False},
'globals_hofs': {'name': 'Globals/HOFs', 'icon': 'package', 'default': False},
'play_time': {'name': 'Session Time', 'icon': 'clock', 'default': False},
}
def initialize(self):
"""Setup dashboard."""
self.widgets = []
self.config_file = Path("data/dashboard_config.json")
self.config_file.parent.mkdir(parents=True, exist_ok=True)
self.enabled_widgets = []
self.widget_data = {}
self._load_config()
self._load_data()
# Auto-refresh timer
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh_data)
self.refresh_timer.start(5000) # Refresh every 5 seconds
def _load_config(self):
"""Load widget configuration."""
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
config = json.load(f)
self.enabled_widgets = config.get('enabled', [])
except:
pass
# Default: enable default widgets
if not self.enabled_widgets:
self.enabled_widgets = [
k for k, v in self.AVAILABLE_WIDGETS.items() if v['default']
]
def _save_config(self):
"""Save widget configuration."""
with open(self.config_file, 'w') as f:
json.dump({'enabled': self.enabled_widgets}, f)
def _load_data(self):
"""Load data from other plugins."""
# Try to get data from other plugin files
data_dir = Path("data")
# PED from inventory
inv_file = data_dir / "inventory.json"
if inv_file.exists():
try:
with open(inv_file, 'r') as f:
data = json.load(f)
items = data.get('items', [])
total_tt = sum(item.get('tt', 0) for item in items)
self.widget_data['ped_balance'] = total_tt
except:
self.widget_data['ped_balance'] = 0
# Skills
skills_file = data_dir / "skill_tracker.json"
if skills_file.exists():
try:
with open(skills_file, 'r') as f:
data = json.load(f)
self.widget_data['skill_count'] = len(data.get('skills', {}))
self.widget_data['total_gains_today'] = len([
g for g in data.get('gains', [])
if datetime.fromisoformat(g['time']).date() == datetime.now().date()
])
except:
self.widget_data['skill_count'] = 0
self.widget_data['total_gains_today'] = 0
# Inventory count
if inv_file.exists():
try:
with open(inv_file, 'r') as f:
data = json.load(f)
self.widget_data['inventory_items'] = len(data.get('items', []))
except:
self.widget_data['inventory_items'] = 0
# Professions
prof_file = data_dir / "professions.json"
if prof_file.exists():
try:
with open(prof_file, 'r') as f:
data = json.load(f)
self.widget_data['professions_count'] = len(data.get('professions', {}))
except:
self.widget_data['professions_count'] = 0
def _refresh_data(self):
"""Refresh widget data."""
self._load_data()
if hasattr(self, 'widgets_container'):
self._update_widgets()
def get_ui(self):
"""Create dashboard UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
# Main scroll area
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setStyleSheet("""
QScrollArea {
background: transparent;
border: none;
}
QScrollBar:vertical {
background: rgba(0, 0, 0, 50);
width: 8px;
border-radius: 4px;
}
QScrollBar::handle:vertical {
background: rgba(255, 255, 255, 30);
border-radius: 4px;
}
""")
container = QWidget()
layout = QVBoxLayout(container)
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Welcome
welcome = QLabel(" Welcome to EU-Utility")
welcome.setStyleSheet(f"""
color: {EU_COLORS['accent_orange']};
font-size: 18px;
font-weight: bold;
""")
layout.addWidget(welcome)
# Header with customize button
header = QHBoxLayout()
subtitle = QLabel("Your Entropia Universe companion")
subtitle.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 12px;")
layout.addWidget(subtitle)
title = QLabel("Dashboard")
title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
header.addWidget(title)
# Quick stats row
stats_layout = QHBoxLayout()
stats_layout.setSpacing(10)
header.addStretch()
stats = [
("💰 PED", "26.02", "Balance"),
("Skills", "12", "Tracked"),
("Items", "98", "In Inventory"),
("🎯 DPP", "3.45", "Current"),
]
for icon, value, label in stats:
card = self._create_stat_card(icon, value, label)
stats_layout.addWidget(card)
layout.addLayout(stats_layout)
# Quick actions
actions_label = QLabel("Quick Actions")
actions_label.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 13px; font-weight: bold;")
layout.addWidget(actions_label)
actions_grid = QGridLayout()
actions_grid.setSpacing(10)
actions = [
("Search", "Search items, mobs, locations"),
("Scan", "OCR scan game windows"),
("Skills", "Track skill gains"),
("Loot", "Track hunting loot"),
("⛏️ Mine", "Track mining finds"),
("📈 Market", "Auction price tracking"),
]
for i, (name, desc) in enumerate(actions):
btn = self._create_action_button(name, desc)
actions_grid.addWidget(btn, i // 3, i % 3)
layout.addLayout(actions_grid)
# Recent activity
activity_label = QLabel("Recent Activity")
activity_label.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 13px; font-weight: bold; margin-top: 10px;")
layout.addWidget(activity_label)
activity_frame = QFrame()
activity_frame.setStyleSheet(f"""
QFrame {{
customize_btn = QPushButton("Customize")
customize_btn.setStyleSheet(f"""
QPushButton {{
background-color: {EU_COLORS['bg_panel']};
color: {EU_COLORS['text_secondary']};
border: 1px solid {EU_COLORS['border_subtle']};
border-radius: 6px;
border-radius: 4px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {EU_COLORS['bg_hover']};
border-color: {EU_COLORS['accent_orange']};
}}
""")
activity_layout = QVBoxLayout(activity_frame)
customize_btn.clicked.connect(self._show_customize_dialog)
header.addWidget(customize_btn)
activities = [
"Scanned inventory - 26.02 PED",
"Tracked skill gain: +5.2 Aim",
"Recorded loot: Animal Hide (0.03 PED)",
"→ Mission progress: 12/100 Oratan",
]
layout.addLayout(header)
for activity in activities:
lbl = QLabel(activity)
lbl.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 11px; padding: 4px 0;")
activity_layout.addWidget(lbl)
# Scroll area for widgets
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
layout.addWidget(activity_frame)
self.widgets_container = QWidget()
self.widgets_layout = QGridLayout(self.widgets_container)
self.widgets_layout.setSpacing(15)
self.widgets_layout.setContentsMargins(0, 0, 0, 0)
# Tips
tips_frame = QFrame()
tips_frame.setStyleSheet(f"""
QFrame {{
background-color: rgba(255, 140, 66, 20);
border: 1px solid rgba(255, 140, 66, 60);
border-radius: 6px;
}}
""")
tips_layout = QVBoxLayout(tips_frame)
self._update_widgets()
tip_title = QLabel("Pro Tip")
tip_title.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-weight: bold; font-size: 11px;")
tips_layout.addWidget(tip_title)
tip_text = QLabel("Press Ctrl+Shift+U anytime to toggle this overlay. Use Ctrl+Shift+H to hide all widgets.")
tip_text.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 11px;")
tip_text.setWordWrap(True)
tips_layout.addWidget(tip_text)
layout.addWidget(tips_frame)
layout.addStretch()
scroll.setWidget(container)
main_layout = QVBoxLayout(widget)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(scroll)
scroll.setWidget(self.widgets_container)
layout.addWidget(scroll)
return widget
def _create_stat_card(self, icon, value, label):
"""Create a stat card widget."""
def _update_widgets(self):
"""Update widget display."""
# Clear existing
while self.widgets_layout.count():
item = self.widgets_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Add enabled widgets
col = 0
row = 0
for widget_id in self.enabled_widgets:
if widget_id in self.AVAILABLE_WIDGETS:
widget_info = self.AVAILABLE_WIDGETS[widget_id]
card = self._create_widget_card(
widget_id,
widget_info['name'],
widget_info['icon']
)
self.widgets_layout.addWidget(card, row, col)
col += 1
if col >= 2: # 2 columns
col = 0
row += 1
def _create_widget_card(self, widget_id, name, icon_name):
"""Create a stat widget card."""
card = QFrame()
card.setStyleSheet(f"""
QFrame {{
background-color: {EU_COLORS['bg_panel']};
border: 1px solid {EU_COLORS['border_subtle']};
border-radius: 6px;
border-radius: 8px;
}}
""")
layout = QVBoxLayout(card)
layout.setContentsMargins(12, 10, 12, 10)
layout.setSpacing(4)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(8)
value_lbl = QLabel(f"{icon} {value}")
value_lbl.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-size: 16px; font-weight: bold;")
layout.addWidget(value_lbl)
# Title
title = QLabel(name)
title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;")
layout.addWidget(title)
label_lbl = QLabel(label)
label_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 10px;")
layout.addWidget(label_lbl)
# Value
value = self.widget_data.get(widget_id, 0)
if widget_id == 'ped_balance':
value_text = f"{value:.2f} PED"
elif widget_id == 'play_time':
value_text = "2h 34m" # Placeholder
elif widget_id == 'current_dpp':
value_text = "3.45"
else:
value_text = str(value)
value_label = QLabel(value_text)
value_label.setStyleSheet(f"""
color: {EU_COLORS['accent_orange']};
font-size: 24px;
font-weight: bold;
""")
layout.addWidget(value_label)
layout.addStretch()
return card
def _create_action_button(self, name, description):
"""Create an action button."""
btn = QPushButton()
btn.setFixedHeight(60)
btn.setStyleSheet(f"""
QPushButton {{
background-color: {EU_COLORS['bg_panel']};
border: 1px solid {EU_COLORS['border_subtle']};
border-radius: 6px;
text-align: left;
padding: 10px;
def _show_customize_dialog(self):
"""Show widget customization dialog."""
dialog = QDialog()
dialog.setWindowTitle("Customize Dashboard")
dialog.setStyleSheet(f"""
QDialog {{
background-color: {EU_COLORS['bg_dark']};
color: white;
}}
QPushButton:hover {{
background-color: {EU_COLORS['bg_hover']};
border: 1px solid {EU_COLORS['border_orange']};
QLabel {{
color: white;
}}
""")
# Layout for button content
btn_widget = QWidget()
btn_layout = QVBoxLayout(btn_widget)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(2)
layout = QVBoxLayout(dialog)
name_lbl = QLabel(name)
name_lbl.setStyleSheet(f"color: white; font-weight: bold; font-size: 12px;")
btn_layout.addWidget(name_lbl)
# Instructions
info = QLabel("Check widgets to display on dashboard:")
info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};")
layout.addWidget(info)
desc_lbl = QLabel(description)
desc_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 9px;")
btn_layout.addWidget(desc_lbl)
# Widget list
list_widget = QListWidget()
list_widget.setStyleSheet(f"""
QListWidget {{
background-color: {EU_COLORS['bg_panel']};
color: white;
border: 1px solid {EU_COLORS['border_subtle']};
}}
QListWidget::item {{
padding: 10px;
}}
""")
return btn
for widget_id, widget_info in self.AVAILABLE_WIDGETS.items():
item = QListWidgetItem(widget_info['name'])
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(
Qt.CheckState.Checked if widget_id in self.enabled_widgets
else Qt.CheckState.Unchecked
)
item.setData(Qt.ItemDataRole.UserRole, widget_id)
list_widget.addItem(item)
layout.addWidget(list_widget)
# Buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
if dialog.exec() == QDialog.DialogCode.Accepted:
# Save selection
self.enabled_widgets = []
for i in range(list_widget.count()):
item = list_widget.item(i)
if item.checkState() == Qt.CheckState.Checked:
self.enabled_widgets.append(item.data(Qt.ItemDataRole.UserRole))
self._save_config()
self._update_widgets()

View File

@ -0,0 +1,7 @@
"""
Profession Scanner Plugin
"""
from .plugin import ProfessionScannerPlugin
__all__ = ["ProfessionScannerPlugin"]

View File

@ -0,0 +1,247 @@
"""
EU-Utility - Profession Scanner Plugin
Scan and track profession progress with OCR.
"""
import re
import json
from datetime import datetime
from pathlib import Path
from decimal import Decimal
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
QFrame, QGroupBox, QComboBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin
class ProfessionOCRThread(QThread):
"""OCR scan for professions window."""
scan_complete = pyqtSignal(dict)
scan_error = pyqtSignal(str)
progress_update = pyqtSignal(str)
def run(self):
"""Perform OCR scan."""
try:
self.progress_update.emit("Capturing screen...")
import pyautogui
screenshot = pyautogui.screenshot()
self.progress_update.emit("Running OCR...")
# OCR
try:
import easyocr
reader = easyocr.Reader(['en'], verbose=False)
results = reader.readtext(screenshot)
text = '\n'.join([r[1] for r in results])
except:
import pytesseract
from PIL import Image
text = pytesseract.image_to_string(screenshot)
# Parse professions
professions = self._parse_professions(text)
self.scan_complete.emit(professions)
except Exception as e:
self.scan_error.emit(str(e))
def _parse_professions(self, text):
"""Parse profession data from OCR text."""
professions = {}
# Pattern: ProfessionName Rank %Progress
# Example: "Laser Pistoleer (Hit) Elite, 72 68.3%"
lines = text.split('\n')
for line in lines:
# Match profession with rank and percentage
match = re.search(r'(\w+(?:\s+\w+)*)\s+\(?(\w+)?\)?\s+(Elite|Champion|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome),?\s+(\d+)[,\s]+(\d+\.?\d*)%?', line)
if match:
prof_name = match.group(1).strip()
spec = match.group(2) or ""
rank_name = match.group(3)
rank_num = match.group(4)
progress = float(match.group(5))
full_name = f"{prof_name} ({spec})" if spec else prof_name
professions[full_name] = {
'rank_name': rank_name,
'rank_num': int(rank_num),
'progress': progress,
'scanned_at': datetime.now().isoformat()
}
return professions
class ProfessionScannerPlugin(BasePlugin):
"""Scan and track profession progress."""
name = "Profession Scanner"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Track profession ranks and progress"
hotkey = "ctrl+shift+p"
def initialize(self):
"""Setup profession scanner."""
self.data_file = Path("data/professions.json")
self.data_file.parent.mkdir(parents=True, exist_ok=True)
self.professions = {}
self._load_data()
def _load_data(self):
"""Load saved data."""
if self.data_file.exists():
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
self.professions = data.get('professions', {})
except:
pass
def _save_data(self):
"""Save data."""
with open(self.data_file, 'w') as f:
json.dump({'professions': self.professions}, f, indent=2)
def get_ui(self):
"""Create profession scanner UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Header
header = QLabel("Profession Tracker")
header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
layout.addWidget(header)
# Summary
summary = QHBoxLayout()
self.total_label = QLabel(f"Professions: {len(self.professions)}")
self.total_label.setStyleSheet("color: #4ecdc4; font-weight: bold;")
summary.addWidget(self.total_label)
summary.addStretch()
layout.addLayout(summary)
# Scan button
scan_btn = QPushButton("Scan Professions Window")
scan_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: white;
padding: 12px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
scan_btn.clicked.connect(self._scan_professions)
layout.addWidget(scan_btn)
# Progress
self.progress_label = QLabel("Ready to scan")
self.progress_label.setStyleSheet("color: rgba(255,255,255,150);")
layout.addWidget(self.progress_label)
# Professions table
self.prof_table = QTableWidget()
self.prof_table.setColumnCount(4)
self.prof_table.setHorizontalHeaderLabels(["Profession", "Rank", "Level", "Progress"])
self.prof_table.horizontalHeader().setStretchLastSection(True)
# Style table
self.prof_table.setStyleSheet("""
QTableWidget {
background-color: rgba(30, 35, 45, 200);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 6px;
}
QHeaderView::section {
background-color: rgba(35, 40, 55, 200);
color: rgba(255,255,255,180);
padding: 8px;
font-weight: bold;
}
""")
layout.addWidget(self.prof_table)
# Refresh table
self._refresh_table()
return widget
def _scan_professions(self):
"""Start OCR scan."""
self.scanner = ProfessionOCRThread()
self.scanner.scan_complete.connect(self._on_scan_complete)
self.scanner.scan_error.connect(self._on_scan_error)
self.scanner.progress_update.connect(self._on_progress)
self.scanner.start()
def _on_progress(self, message):
"""Update progress."""
self.progress_label.setText(message)
def _on_scan_complete(self, professions):
"""Handle scan completion."""
self.professions.update(professions)
self._save_data()
self._refresh_table()
self.progress_label.setText(f"Found {len(professions)} professions")
self.total_label.setText(f"Professions: {len(self.professions)}")
def _on_scan_error(self, error):
"""Handle error."""
self.progress_label.setText(f"Error: {error}")
def _refresh_table(self):
"""Refresh professions table."""
self.prof_table.setRowCount(len(self.professions))
for i, (name, data) in enumerate(sorted(self.professions.items())):
self.prof_table.setItem(i, 0, QTableWidgetItem(name))
self.prof_table.setItem(i, 1, QTableWidgetItem(data.get('rank_name', '-')))
self.prof_table.setItem(i, 2, QTableWidgetItem(str(data.get('rank_num', 0))))
# Progress with bar
progress = data.get('progress', 0)
progress_widget = QWidget()
progress_layout = QHBoxLayout(progress_widget)
progress_layout.setContentsMargins(5, 2, 5, 2)
bar = QProgressBar()
bar.setValue(int(progress))
bar.setTextVisible(True)
bar.setFormat(f"{progress:.1f}%")
bar.setStyleSheet("""
QProgressBar {
background-color: rgba(60, 70, 90, 150);
border: none;
border-radius: 3px;
text-align: center;
color: white;
}
QProgressBar::chunk {
background-color: #ff8c42;
border-radius: 3px;
}
""")
progress_layout.addWidget(bar)
self.prof_table.setCellWidget(i, 3, progress_widget)

View File

@ -1,673 +1,333 @@
"""
EU-Utility - Skill Scanner Plugin
EU-Utility - Skill Scanner Plugin with OCR + Log Tracking
Tracks skill levels with decimal precision by reading:
1. Skills window (OCR for values + progress bar detection)
2. ESI item windows (skill gain prediction)
3. Chat log (skill gain messages)
Data stored for skill formula analysis.
Scans skills window AND tracks gains from chat log automatically.
"""
import re
import json
import time
import threading
from datetime import datetime
from pathlib import Path
from decimal import Decimal
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QTextEdit, QComboBox, QSpinBox, QGroupBox,
QFileDialog, QMessageBox, QTabWidget
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
QFrame, QGroupBox, QTextEdit, QSplitter
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QColor
from plugins.base_plugin import BasePlugin
class SkillDataStore:
"""Store skill data for formula analysis."""
class LogWatcherThread(QThread):
"""Watch chat log for skill gains."""
skill_gain_detected = pyqtSignal(str, float, float) # skill_name, gain_amount, new_total
def __init__(self, data_dir="data/skills"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.skills_file = self.data_dir / "skill_snapshots.json"
self.esi_file = self.data_dir / "esi_data.json"
self.gains_file = self.data_dir / "skill_gains.json"
def __init__(self, log_path, parent=None):
super().__init__(parent)
self.log_path = Path(log_path)
self.running = True
self.last_position = 0
def save_skill_snapshot(self, skills_data):
"""Save a skill snapshot with timestamp."""
snapshot = {
"timestamp": datetime.now().isoformat(),
"skills": skills_data
}
# Append to history
history = self.load_skill_history()
history.append(snapshot)
# Keep last 1000 snapshots
if len(history) > 1000:
history = history[-1000:]
with open(self.skills_file, 'w') as f:
json.dump(history, f, indent=2)
def load_skill_history(self):
"""Load skill history."""
if self.skills_file.exists():
with open(self.skills_file, 'r') as f:
return json.load(f)
return []
def save_esi_data(self, esi_data):
"""Save ESI scan data."""
entry = {
"timestamp": datetime.now().isoformat(),
"esi": esi_data
}
history = self.load_esi_history()
history.append(entry)
with open(self.esi_file, 'w') as f:
json.dump(history, f, indent=2)
def load_esi_history(self):
"""Load ESI history."""
if self.esi_file.exists():
with open(self.esi_file, 'r') as f:
return json.load(f)
return []
def save_skill_gain(self, skill_name, points_gained, new_total=None):
"""Save a skill gain from chat."""
entry = {
"timestamp": datetime.now().isoformat(),
"skill": skill_name,
"gained": points_gained,
"new_total": new_total
}
history = self.load_gain_history()
history.append(entry)
with open(self.gains_file, 'w') as f:
json.dump(history, f, indent=2)
def load_gain_history(self):
"""Load skill gain history."""
if self.gains_file.exists():
with open(self.gains_file, 'r') as f:
return json.load(f)
return []
def export_to_csv(self, filepath):
"""Export all data to CSV for analysis."""
import csv
with open(filepath, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['timestamp', 'skill', 'rank', 'points', 'progress_pct', 'source'])
# Export skill snapshots
for snapshot in self.load_skill_history():
ts = snapshot['timestamp']
for skill_name, data in snapshot['skills'].items():
writer.writerow([
ts,
skill_name,
data.get('rank', ''),
data.get('points', ''),
data.get('progress_pct', ''),
'snapshot'
])
# Export skill gains
for gain in self.load_gain_history():
writer.writerow([
gain['timestamp'],
gain['skill'],
'',
gain.get('gained', ''),
'',
'chat_gain'
])
class SkillScannerThread(QThread):
"""Background thread for skill window OCR."""
result_ready = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
progress_update = pyqtSignal(str)
def __init__(self, capture_mode="full_screen"):
super().__init__()
self.capture_mode = capture_mode
# Skill gain patterns
self.patterns = [
r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+\.?\d*)\s+points?', # "Aim has improved by 5.2 points"
r'You\s+gained\s+(\d+\.?\d*)\s+points?\s+in\s+(\w+(?:\s+\w+)*)', # "You gained 10 points in Rifle"
r'(\w+(?:\s+\w+)*)\s+\+(\d+\.?\d*)', # "Rifle +15"
]
def run(self):
"""Capture screen and extract skill data."""
"""Watch log file for changes."""
while self.running:
try:
if self.log_path.exists():
with open(self.log_path, 'r', encoding='utf-8', errors='ignore') as f:
f.seek(self.last_position)
new_lines = f.readlines()
self.last_position = f.tell()
for line in new_lines:
self._parse_line(line.strip())
except Exception as e:
print(f"[LogWatcher] Error: {e}")
time.sleep(0.5) # Check every 500ms
def _parse_line(self, line):
"""Parse a log line for skill gains."""
for pattern in self.patterns:
match = re.search(pattern, line, re.IGNORECASE)
if match:
groups = match.groups()
if len(groups) == 2:
# Determine which group is skill and which is value
try:
value = float(groups[1])
skill = groups[0]
self.skill_gain_detected.emit(skill, value, 0) # new_total calculated later
except ValueError:
# Try reversed
try:
value = float(groups[0])
skill = groups[1]
self.skill_gain_detected.emit(skill, value, 0)
except:
pass
break
def stop(self):
"""Stop watching."""
self.running = False
class OCRScannerThread(QThread):
"""OCR scan thread for skills window."""
scan_complete = pyqtSignal(dict)
scan_error = pyqtSignal(str)
progress_update = pyqtSignal(str)
def run(self):
"""Perform OCR scan."""
try:
self.progress_update.emit("Capturing screen...")
# Capture screen
screenshot = self._capture_screen()
if screenshot is None:
self.error_occurred.emit("Failed to capture screen")
return
self.progress_update.emit("Analyzing skills window...")
# Extract skill data
skills_data = self._extract_skills(screenshot)
self.progress_update.emit(f"✅ Found {len(skills_data)} skills")
self.result_ready.emit(skills_data)
except Exception as e:
self.error_occurred.emit(str(e))
def _capture_screen(self):
"""Capture full screen."""
try:
import pyautogui
return pyautogui.screenshot()
except ImportError:
# Fallback to PIL
from PIL import ImageGrab
return ImageGrab.grab()
def _extract_skills(self, screenshot):
"""Extract skill data from screenshot."""
skills_data = {}
# Perform OCR
try:
import easyocr
reader = easyocr.Reader(['en'], verbose=False)
results = reader.readtext(screenshot)
text_lines = [result[1] for result in results]
full_text = ' '.join(text_lines)
# Check if this is a skills window
if 'SKILLS' not in full_text.upper() and 'RANK' not in full_text.upper():
self.progress_update.emit("⚠️ Skills window not detected")
return skills_data
# Extract skill lines
# Pattern: SkillName Rank Points
for i, line in enumerate(text_lines):
# Look for rank keywords
rank_match = self._match_rank(line)
if rank_match:
# Previous line might be skill name
if i > 0:
skill_name = text_lines[i-1].strip()
rank = rank_match
# Next line might be points
points = 0
if i + 1 < len(text_lines):
points_str = text_lines[i+1].replace(',', '').replace('.', '')
if points_str.isdigit():
points = int(points_str)
# Detect progress bar percentage (approximate from image)
progress_pct = self._estimate_progress(screenshot, i)
skills_data[skill_name] = {
'rank': rank,
'points': points,
'progress_pct': progress_pct,
'raw_text': line
}
except Exception as e:
self.progress_update.emit(f"OCR error: {e}")
return skills_data
def _match_rank(self, text):
"""Match rank text."""
ranks = [
'Newbie', 'Inept', 'Green', 'Beginner', 'Initiated', 'Trainee',
'Apprentice', 'Assistant', 'Assistant', 'Capable', 'Competent',
'Skilled', 'Talented', 'Expert', 'Grand', 'Great', 'Adept',
'Professional', 'Illustrious', 'Eminent', 'Renowned', 'Master',
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
'Outstanding', 'Marvelous', 'Prodigious', 'Staggering', 'Unrivaled',
'Amazing', 'Incredible', 'Awesome', 'Mind-boggling', 'Sensational',
'Miraculous', 'Colossal', 'Epic', 'Transcendent', 'Magnificent',
'Unearthly', 'Phenomenal', 'Supreme', 'Omnipotent'
]
for rank in ranks:
if rank.lower() in text.lower():
return rank
return None
def _estimate_progress(self, screenshot, line_index):
"""Estimate progress bar percentage from image analysis."""
# This would analyze the green bar length
# For now, return 0 (can be improved with image processing)
return 0.0
class ESIScannerThread(QThread):
"""Scan ESI item window for skill gain prediction."""
result_ready = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
def run(self):
"""Capture and analyze ESI window."""
try:
# Use pyautogui for screenshot
import pyautogui
screenshot = pyautogui.screenshot()
# OCR
import easyocr
reader = easyocr.Reader(['en'], verbose=False)
results = reader.readtext(screenshot)
self.progress_update.emit("Running OCR...")
text = ' '.join([r[1] for r in results])
# Try easyocr first
try:
import easyocr
reader = easyocr.Reader(['en'], verbose=False)
results = reader.readtext(screenshot)
text = '\n'.join([r[1] for r in results])
except:
# Fallback to pytesseract
import pytesseract
from PIL import Image
text = pytesseract.image_to_string(screenshot)
# Look for ESI patterns
esi_data = {
'item_name': None,
'skill_target': None,
'points_to_add': 0,
'tt_value': 0.0,
'full_text': text
}
# Extract ESI info
if 'Empty Skill Implant' in text or 'ESI' in text:
# Try to find skill name and points
# Pattern: "Inserting this implant will add X points to [SkillName]"
patterns = [
r'add\s+(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
r'(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
]
for pattern in patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
esi_data['points_to_add'] = int(match.group(1))
esi_data['skill_target'] = match.group(2).strip()
break
# Extract TT value
tt_match = re.search(r'(\d+\.?\d*)\s*PED', text)
if tt_match:
esi_data['tt_value'] = float(tt_match.group(1))
self.result_ready.emit(esi_data)
# Parse skills from text
skills_data = self._parse_skills(text)
self.scan_complete.emit(skills_data)
except Exception as e:
self.error_occurred.emit(str(e))
self.scan_error.emit(str(e))
def _parse_skills(self, text):
"""Parse skill data from OCR text."""
skills = {}
lines = text.split('\n')
# Look for skill patterns
# Example: "Aim Amazing 5524"
for line in lines:
# Pattern: SkillName Rank Points
match = re.search(r'(\w+(?:\s+\w+)*)\s+(Newbie|Inept|Beginner|Amateur|Average|Skilled|Expert|Professional|Master|Grand Master|Champion|Legendary|Guru|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome)\s+(\d+)', line, re.IGNORECASE)
if match:
skill_name = match.group(1).strip()
rank = match.group(2)
points = int(match.group(3))
skills[skill_name] = {
'rank': rank,
'points': points,
'scanned_at': datetime.now().isoformat()
}
return skills
class SkillScannerPlugin(BasePlugin):
"""Scan and track skill progression with decimal precision."""
"""Scan skills with OCR and track gains from log."""
name = "Skill Scanner"
version = "1.0.0"
version = "2.0.0"
author = "ImpulsiveFPS"
description = "Track skill levels with precision for formula analysis"
hotkey = "ctrl+shift+s" # S for Skills
description = "OCR skill scanning + automatic log tracking"
hotkey = "ctrl+shift+s"
def initialize(self):
"""Setup skill scanner."""
self.data_store = SkillDataStore()
self.scan_thread = None
self.esi_thread = None
self.last_scan = {}
self.data_file = Path("data/skill_tracker.json")
self.data_file.parent.mkdir(parents=True, exist_ok=True)
# Load saved data
self.skills_data = {}
self.skill_gains = []
self._load_data()
# Start log watcher
log_path = self._find_chat_log()
if log_path:
self.log_watcher = LogWatcherThread(log_path)
self.log_watcher.skill_gain_detected.connect(self._on_skill_gain)
self.log_watcher.start()
else:
self.log_watcher = None
def _find_chat_log(self):
"""Find EU chat log file."""
# Common locations
possible_paths = [
Path.home() / "Documents" / "Entropia Universe" / "chat.log",
Path.home() / "Documents" / "Entropia Universe" / "Logs" / "chat.log",
]
for path in possible_paths:
if path.exists():
return path
return None
def _load_data(self):
"""Load saved skill data."""
if self.data_file.exists():
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
self.skills_data = data.get('skills', {})
self.skill_gains = data.get('gains', [])
except:
pass
def _save_data(self):
"""Save skill data."""
with open(self.data_file, 'w') as f:
json.dump({
'skills': self.skills_data,
'gains': self.skill_gains
}, f, indent=2)
def _on_skill_gain(self, skill_name, gain_amount, new_total):
"""Handle skill gain from log."""
# Update skill data
if skill_name in self.skills_data:
old_points = self.skills_data[skill_name].get('points', 0)
self.skills_data[skill_name]['points'] = old_points + gain_amount
self.skills_data[skill_name]['last_gain'] = {
'amount': gain_amount,
'time': datetime.now().isoformat()
}
# Record gain
self.skill_gains.append({
'skill': skill_name,
'gain': gain_amount,
'time': datetime.now().isoformat()
})
self._save_data()
# Update UI if visible
if hasattr(self, 'gains_text'):
self.gains_text.append(f"+{gain_amount} {skill_name}")
def get_ui(self):
"""Create skill scanner UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("Skill Scanner")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Header
header = QLabel("Skill Tracker")
header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
layout.addWidget(header)
# Info
info = QLabel("Scan skills window to track progression with decimal precision")
info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
layout.addWidget(info)
# Splitter for resizable sections
splitter = QSplitter(Qt.Orientation.Vertical)
# Tabs
tabs = QTabWidget()
tabs.setStyleSheet("""
QTabWidget::pane {
background-color: rgba(0, 0, 0, 50);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 20);
}
QTabBar::tab {
background-color: rgba(255, 255, 255, 10);
color: rgba(255, 255, 255, 150);
padding: 8px 16px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
QTabBar::tab:selected {
background-color: rgba(74, 158, 255, 150);
color: white;
}
""")
# Scan section
scan_group = QGroupBox("OCR Scan")
scan_layout = QVBoxLayout(scan_group)
# Skills Tab
skills_tab = self._create_skills_tab()
tabs.addTab(skills_tab, "🎯 Skills")
# ESI Tab
esi_tab = self._create_esi_tab()
tabs.addTab(esi_tab, "💉 ESI Scanner")
# Data Tab
data_tab = self._create_data_tab()
tabs.addTab(data_tab, "📈 Data")
layout.addWidget(tabs)
return widget
def _create_skills_tab(self):
"""Create skills scanning tab."""
tab = QWidget()
tab.setStyleSheet("background: transparent;")
layout = QVBoxLayout(tab)
layout.setSpacing(10)
layout.setContentsMargins(10, 10, 10, 10)
# Scan button
scan_btn = QPushButton("Scan Skills Window")
scan_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
background-color: #ff8c42;
color: white;
padding: 15px;
padding: 12px;
border: none;
border-radius: 10px;
font-size: 14px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #5aafff;
}
""")
scan_btn.clicked.connect(self._scan_skills)
layout.addWidget(scan_btn)
scan_layout.addWidget(scan_btn)
# Status
self.status_label = QLabel("Ready to scan")
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.status_label)
# Progress
self.scan_progress = QLabel("Ready to scan")
self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
scan_layout.addWidget(self.scan_progress)
# Results table
# Skills table
self.skills_table = QTableWidget()
self.skills_table.setColumnCount(4)
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points", "Progress"])
self.skills_table.setStyleSheet("""
QTableWidget {
background-color: rgba(30, 30, 30, 100);
color: white;
border: none;
border-radius: 6px;
gridline-color: rgba(255, 255, 255, 20);
}
QHeaderView::section {
background-color: rgba(74, 158, 255, 100);
color: white;
padding: 6px;
border: none;
}
""")
self.skills_table.setColumnCount(3)
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
self.skills_table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(self.skills_table)
scan_layout.addWidget(self.skills_table)
return tab
splitter.addWidget(scan_group)
def _create_esi_tab(self):
"""Create ESI scanning tab."""
tab = QWidget()
tab.setStyleSheet("background: transparent;")
layout = QVBoxLayout(tab)
layout.setSpacing(10)
layout.setContentsMargins(10, 10, 10, 10)
# Log tracking section
log_group = QGroupBox("Automatic Log Tracking")
log_layout = QVBoxLayout(log_group)
# Instructions
instr = QLabel("Hover over an ESI item and click scan")
instr.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
layout.addWidget(instr)
log_status = QLabel("Watching chat log for skill gains...")
log_status.setStyleSheet("color: #4ecdc4;")
log_layout.addWidget(log_status)
# Scan ESI button
esi_btn = QPushButton("💉 Scan ESI Item")
esi_btn.setStyleSheet("""
QPushButton {
background-color: #9c27b0;
color: white;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #ab47bc;
}
""")
esi_btn.clicked.connect(self._scan_esi)
layout.addWidget(esi_btn)
self.gains_text = QTextEdit()
self.gains_text.setReadOnly(True)
self.gains_text.setMaximumHeight(150)
self.gains_text.setPlaceholderText("Recent skill gains will appear here...")
log_layout.addWidget(self.gains_text)
# ESI Results
self.esi_result = QTextEdit()
self.esi_result.setPlaceholderText("ESI scan results will appear here...")
self.esi_result.setStyleSheet("""
QTextEdit {
background-color: rgba(30, 30, 30, 100);
color: white;
border: none;
border-radius: 6px;
padding: 8px;
}
""")
self.esi_result.setMaximumHeight(150)
layout.addWidget(self.esi_result)
# Total gains summary
self.total_gains_label = QLabel(f"Total gains tracked: {len(self.skill_gains)}")
self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);")
log_layout.addWidget(self.total_gains_label)
layout.addStretch()
return tab
splitter.addWidget(log_group)
def _create_data_tab(self):
"""Create data management tab."""
tab = QWidget()
tab.setStyleSheet("background: transparent;")
layout = QVBoxLayout(tab)
layout.setSpacing(10)
layout.setContentsMargins(10, 10, 10, 10)
layout.addWidget(splitter)
# Stats
self.stats_label = QLabel("No data collected yet")
self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 200);")
layout.addWidget(self.stats_label)
# Load existing data
self._refresh_skills_table()
# Export button
export_btn = QPushButton("📁 Export to CSV")
export_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 20);
color: white;
padding: 10px;
border: none;
border-radius: 6px;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 30);
}
""")
export_btn.clicked.connect(self._export_data)
layout.addWidget(export_btn)
# View raw data
view_btn = QPushButton("View Raw JSON")
view_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 20);
color: white;
padding: 10px;
border: none;
border-radius: 6px;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 30);
}
""")
view_btn.clicked.connect(self._view_raw_data)
layout.addWidget(view_btn)
layout.addStretch()
return tab
return widget
def _scan_skills(self):
"""Start skills scan."""
self.status_label.setText("Capturing...")
self.status_label.setStyleSheet("color: #4a9eff;")
self.scan_thread = SkillScannerThread()
self.scan_thread.result_ready.connect(self._on_skills_scanned)
self.scan_thread.error_occurred.connect(self._on_scan_error)
self.scan_thread.progress_update.connect(self._on_scan_progress)
self.scan_thread.start()
"""Start OCR scan."""
self.scanner = OCRScannerThread()
self.scanner.scan_complete.connect(self._on_scan_complete)
self.scanner.scan_error.connect(self._on_scan_error)
self.scanner.progress_update.connect(self._on_scan_progress)
self.scanner.start()
def _on_scan_progress(self, message):
"""Update scan progress."""
self.status_label.setText(message)
self.scan_progress.setText(message)
def _on_skills_scanned(self, skills_data):
"""Handle scanned skills."""
self.last_scan = skills_data
# Save to data store
self.data_store.save_skill_snapshot(skills_data)
# Update table
self.skills_table.setRowCount(len(skills_data))
for i, (name, data) in enumerate(skills_data.items()):
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '')))
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
progress = data.get('progress_pct', 0)
self.skills_table.setItem(i, 3, QTableWidgetItem(f"{progress:.1f}%"))
self.status_label.setText(f"✅ Scanned {len(skills_data)} skills")
self.status_label.setStyleSheet("color: #4caf50;")
self._update_stats()
def _on_scan_complete(self, skills_data):
"""Handle scan completion."""
self.skills_data.update(skills_data)
self._save_data()
self._refresh_skills_table()
self.scan_progress.setText(f"Found {len(skills_data)} skills")
def _on_scan_error(self, error):
"""Handle scan error."""
self.status_label.setText(f"❌ Error: {error}")
self.status_label.setStyleSheet("color: #f44336;")
self.scan_progress.setText(f"Error: {error}")
def _scan_esi(self):
"""Scan ESI item."""
self.esi_thread = ESIScannerThread()
self.esi_thread.result_ready.connect(self._on_esi_scanned)
self.esi_thread.error_occurred.connect(self._on_scan_error)
self.esi_thread.start()
def _on_esi_scanned(self, esi_data):
"""Handle ESI scan result."""
self.data_store.save_esi_data(esi_data)
# Display results
text = f"""
🎯 Skill Target: {esi_data['skill_target'] or 'Unknown'}
Points to Add: {esi_data['points_to_add']}
💰 TT Value: {esi_data['tt_value']:.2f} PED
Raw Text Preview:
{esi_data['full_text'][:200]}...
"""
self.esi_result.setText(text)
self._update_stats()
def _update_stats(self):
"""Update statistics display."""
skill_count = len(self.data_store.load_skill_history())
esi_count = len(self.data_store.load_esi_history())
gain_count = len(self.data_store.load_gain_history())
self.stats_label.setText(
f"Data Points: {skill_count} skill scans | "
f"{esi_count} ESI scans | "
f"{gain_count} chat gains"
)
def _export_data(self):
"""Export data to CSV."""
from PyQt6.QtWidgets import QFileDialog
filepath, _ = QFileDialog.getSaveFileName(
None, "Export Skill Data", "skill_data.csv", "CSV Files (*.csv)"
)
if filepath:
self.data_store.export_to_csv(filepath)
self.status_label.setText(f"✅ Exported to {filepath}")
def _view_raw_data(self):
"""Open raw data folder."""
import subprocess
import platform
path = self.data_store.data_dir.absolute()
if platform.system() == "Windows":
subprocess.run(["explorer", str(path)])
elif platform.system() == "Darwin":
subprocess.run(["open", str(path)])
else:
subprocess.run(["xdg-open", str(path)])
def on_hotkey(self):
"""Quick scan on hotkey."""
self._scan_skills()
def parse_chat_message(self, message):
"""Parse skill gain from chat message."""
# Patterns for skill gains
patterns = [
r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+(?:\.\d+)?)\s+points?',
r'You\s+gained\s+(\d+(?:\.\d+)?)\s+points?\s+in\s+(\w+(?:\s+\w+)*)',
r'(\w+(?:\s+\w+)*)\s+\+(\d+(?:\.\d+)?)',
]
for pattern in patterns:
match = re.search(pattern, message, re.IGNORECASE)
if match:
# Extract skill and points
groups = match.groups()
if len(groups) == 2:
# Determine which is skill and which is points
skill, points = groups
try:
points_val = float(points)
self.data_store.save_skill_gain(skill, points_val)
print(f"[Skill Scanner] Tracked: {skill} +{points_val}")
except ValueError:
# Might be reversed
try:
points_val = float(skill)
self.data_store.save_skill_gain(points, points_val)
except:
pass
break
def _refresh_skills_table(self):
"""Refresh skills table."""
self.skills_table.setRowCount(len(self.skills_data))
for i, (name, data) in enumerate(sorted(self.skills_data.items())):
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))