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() self.hide_overlay()
def _setup_window(self): def _setup_window(self):
"""Configure window with EU styling - shows in taskbar.""" """Configure window - resizable and shows in taskbar."""
self.setWindowTitle("EU-Utility") self.setWindowTitle("EU-Utility")
# Frameless, but NOT Tool (so it shows in taskbar) # Resizable window (no FramelessWindowHint), stays on top
# WindowStaysOnTopHint makes it stay on top without hiding from taskbar
self.setWindowFlags( self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint Qt.WindowType.WindowStaysOnTopHint
) )
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setMinimumSize(600, 400)
# Clean, game-like size
self.resize(850, 600) self.resize(850, 600)
self._center_window() self._center_window()
@ -337,19 +333,22 @@ class OverlayWindow(QMainWindow):
""" """
def _load_plugins(self): def _load_plugins(self):
"""Load plugins into sidebar and stack.""" """Load plugins into sidebar and stack - FIXED indexing."""
for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()): plugins_list = list(self.plugin_manager.get_all_plugins().items())
for idx, (plugin_id, plugin) in enumerate(plugins_list):
# Get icon name # Get icon name
icon_name = get_plugin_icon_name(plugin.name) icon_name = get_plugin_icon_name(plugin.name)
# Add to list view # Add to list view
list_item = QListWidgetItem(plugin.name) 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) self.plugin_list.addItem(list_item)
# Add to icon grid # Add to icon grid
icon_btn = self._create_icon_button(plugin.name, icon_name, idx) icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
self.icon_grid_layout.addWidget(icon_btn) self.icon_grid_layout.addWidget(icon_btn)
self.plugin_buttons.append(icon_btn) # Track buttons
# Add plugin UI to stack # Add plugin UI to stack
try: 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 ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QGridLayout, QFrame, QScrollArea, QPushButton, QGridLayout, QFrame, QScrollArea,
QSizePolicy QSizePolicy, QCheckBox, QDialog, QListWidget,
QListWidgetItem, QDialogButtonBox
) )
from PyQt6.QtCore import Qt, QTimer 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 from plugins.base_plugin import BasePlugin
class DashboardPlugin(BasePlugin): class DashboardPlugin(BasePlugin):
"""Main dashboard with overview of all features.""" """Customizable dashboard with avatar statistics."""
name = "Dashboard" name = "Dashboard"
version = "1.0.0" version = "2.0.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Overview dashboard with widgets" description = "Customizable start page with avatar stats"
hotkey = "ctrl+shift+home" 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): def initialize(self):
"""Setup dashboard.""" """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): def get_ui(self):
"""Create dashboard UI.""" """Create dashboard UI."""
widget = QWidget() widget = QWidget()
widget.setStyleSheet("background: transparent;") layout = QVBoxLayout(widget)
# 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.setSpacing(15) layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
# Welcome # Header with customize button
welcome = QLabel(" Welcome to EU-Utility") header = QHBoxLayout()
welcome.setStyleSheet(f"""
color: {EU_COLORS['accent_orange']};
font-size: 18px;
font-weight: bold;
""")
layout.addWidget(welcome)
subtitle = QLabel("Your Entropia Universe companion") title = QLabel("Dashboard")
subtitle.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 12px;") title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
layout.addWidget(subtitle) header.addWidget(title)
# Quick stats row header.addStretch()
stats_layout = QHBoxLayout()
stats_layout.setSpacing(10)
stats = [ customize_btn = QPushButton("Customize")
("💰 PED", "26.02", "Balance"), customize_btn.setStyleSheet(f"""
("Skills", "12", "Tracked"), QPushButton {{
("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 {{
background-color: {EU_COLORS['bg_panel']}; background-color: {EU_COLORS['bg_panel']};
color: {EU_COLORS['text_secondary']};
border: 1px solid {EU_COLORS['border_subtle']}; 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 = [ layout.addLayout(header)
"Scanned inventory - 26.02 PED",
"Tracked skill gain: +5.2 Aim",
"Recorded loot: Animal Hide (0.03 PED)",
"→ Mission progress: 12/100 Oratan",
]
for activity in activities: # Scroll area for widgets
lbl = QLabel(activity) scroll = QScrollArea()
lbl.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 11px; padding: 4px 0;") scroll.setWidgetResizable(True)
activity_layout.addWidget(lbl) 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 self._update_widgets()
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)
tip_title = QLabel("Pro Tip") scroll.setWidget(self.widgets_container)
tip_title.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-weight: bold; font-size: 11px;") layout.addWidget(scroll)
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)
return widget return widget
def _create_stat_card(self, icon, value, label): def _update_widgets(self):
"""Create a stat card widget.""" """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 = QFrame()
card.setStyleSheet(f""" card.setStyleSheet(f"""
QFrame {{ QFrame {{
background-color: {EU_COLORS['bg_panel']}; background-color: {EU_COLORS['bg_panel']};
border: 1px solid {EU_COLORS['border_subtle']}; border: 1px solid {EU_COLORS['border_subtle']};
border-radius: 6px; border-radius: 8px;
}} }}
""") """)
layout = QVBoxLayout(card) layout = QVBoxLayout(card)
layout.setContentsMargins(12, 10, 12, 10) layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(4) layout.setSpacing(8)
value_lbl = QLabel(f"{icon} {value}") # Title
value_lbl.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-size: 16px; font-weight: bold;") title = QLabel(name)
layout.addWidget(value_lbl) title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;")
layout.addWidget(title)
label_lbl = QLabel(label) # Value
label_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 10px;") value = self.widget_data.get(widget_id, 0)
layout.addWidget(label_lbl)
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 return card
def _create_action_button(self, name, description): def _show_customize_dialog(self):
"""Create an action button.""" """Show widget customization dialog."""
btn = QPushButton() dialog = QDialog()
btn.setFixedHeight(60) dialog.setWindowTitle("Customize Dashboard")
btn.setStyleSheet(f""" dialog.setStyleSheet(f"""
QPushButton {{ QDialog {{
background-color: {EU_COLORS['bg_panel']}; background-color: {EU_COLORS['bg_dark']};
border: 1px solid {EU_COLORS['border_subtle']}; color: white;
border-radius: 6px;
text-align: left;
padding: 10px;
}} }}
QPushButton:hover {{ QLabel {{
background-color: {EU_COLORS['bg_hover']}; color: white;
border: 1px solid {EU_COLORS['border_orange']};
}} }}
""") """)
# Layout for button content layout = QVBoxLayout(dialog)
btn_widget = QWidget()
btn_layout = QVBoxLayout(btn_widget)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(2)
name_lbl = QLabel(name) # Instructions
name_lbl.setStyleSheet(f"color: white; font-weight: bold; font-size: 12px;") info = QLabel("Check widgets to display on dashboard:")
btn_layout.addWidget(name_lbl) info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};")
layout.addWidget(info)
desc_lbl = QLabel(description) # Widget list
desc_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 9px;") list_widget = QListWidget()
btn_layout.addWidget(desc_lbl) 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: Scans skills window AND tracks gains from chat log automatically.
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.
""" """
import re import re
import json import json
import time
import threading
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from decimal import Decimal
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLabel, QPushButton, QTableWidget, QTableWidgetItem, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
QHeaderView, QTextEdit, QComboBox, QSpinBox, QGroupBox, QFrame, QGroupBox, QTextEdit, QSplitter
QFileDialog, QMessageBox, QTabWidget
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QColor
from plugins.base_plugin import BasePlugin from plugins.base_plugin import BasePlugin
class SkillDataStore: class LogWatcherThread(QThread):
"""Store skill data for formula analysis.""" """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"): def __init__(self, log_path, parent=None):
self.data_dir = Path(data_dir) super().__init__(parent)
self.data_dir.mkdir(parents=True, exist_ok=True) self.log_path = Path(log_path)
self.skills_file = self.data_dir / "skill_snapshots.json" self.running = True
self.esi_file = self.data_dir / "esi_data.json" self.last_position = 0
self.gains_file = self.data_dir / "skill_gains.json"
def save_skill_snapshot(self, skills_data): # Skill gain patterns
"""Save a skill snapshot with timestamp.""" self.patterns = [
snapshot = { r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+\.?\d*)\s+points?', # "Aim has improved by 5.2 points"
"timestamp": datetime.now().isoformat(), r'You\s+gained\s+(\d+\.?\d*)\s+points?\s+in\s+(\w+(?:\s+\w+)*)', # "You gained 10 points in Rifle"
"skills": skills_data r'(\w+(?:\s+\w+)*)\s+\+(\d+\.?\d*)', # "Rifle +15"
} ]
# 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
def run(self): 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: try:
self.progress_update.emit("Capturing screen...") self.progress_update.emit("Capturing screen...")
# Capture screen # Use pyautogui for screenshot
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:
import pyautogui import pyautogui
screenshot = pyautogui.screenshot() screenshot = pyautogui.screenshot()
# OCR self.progress_update.emit("Running OCR...")
import easyocr
reader = easyocr.Reader(['en'], verbose=False)
results = reader.readtext(screenshot)
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 # Parse skills from text
esi_data = { skills_data = self._parse_skills(text)
'item_name': None, self.scan_complete.emit(skills_data)
'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)
except Exception as e: 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): class SkillScannerPlugin(BasePlugin):
"""Scan and track skill progression with decimal precision.""" """Scan skills with OCR and track gains from log."""
name = "Skill Scanner" name = "Skill Scanner"
version = "1.0.0" version = "2.0.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Track skill levels with precision for formula analysis" description = "OCR skill scanning + automatic log tracking"
hotkey = "ctrl+shift+s" # S for Skills hotkey = "ctrl+shift+s"
def initialize(self): def initialize(self):
"""Setup skill scanner.""" """Setup skill scanner."""
self.data_store = SkillDataStore() self.data_file = Path("data/skill_tracker.json")
self.scan_thread = None self.data_file.parent.mkdir(parents=True, exist_ok=True)
self.esi_thread = None
self.last_scan = {} # 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): def get_ui(self):
"""Create skill scanner UI.""" """Create skill scanner UI."""
widget = QWidget() widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget) layout = QVBoxLayout(widget)
layout.setSpacing(15) layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
# Title # Header
title = QLabel("Skill Scanner") header = QLabel("Skill Tracker")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
layout.addWidget(title) layout.addWidget(header)
# Info # Splitter for resizable sections
info = QLabel("Scan skills window to track progression with decimal precision") splitter = QSplitter(Qt.Orientation.Vertical)
info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
layout.addWidget(info)
# Tabs # Scan section
tabs = QTabWidget() scan_group = QGroupBox("OCR Scan")
tabs.setStyleSheet(""" scan_layout = QVBoxLayout(scan_group)
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;
}
""")
# 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 = QPushButton("Scan Skills Window")
scan_btn.setStyleSheet(""" scan_btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: #4a9eff; background-color: #ff8c42;
color: white; color: white;
padding: 15px; padding: 12px;
border: none; border: none;
border-radius: 10px; border-radius: 4px;
font-size: 14px;
font-weight: bold; font-weight: bold;
} }
QPushButton:hover {
background-color: #5aafff;
}
""") """)
scan_btn.clicked.connect(self._scan_skills) scan_btn.clicked.connect(self._scan_skills)
layout.addWidget(scan_btn) scan_layout.addWidget(scan_btn)
# Status # Progress
self.status_label = QLabel("Ready to scan") self.scan_progress = QLabel("Ready to scan")
self.status_label.setStyleSheet("color: #666; font-size: 11px;") self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) scan_layout.addWidget(self.scan_progress)
layout.addWidget(self.status_label)
# Results table # Skills table
self.skills_table = QTableWidget() self.skills_table = QTableWidget()
self.skills_table.setColumnCount(4) self.skills_table.setColumnCount(3)
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points", "Progress"]) self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
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.horizontalHeader().setStretchLastSection(True) 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): # Log tracking section
"""Create ESI scanning tab.""" log_group = QGroupBox("Automatic Log Tracking")
tab = QWidget() log_layout = QVBoxLayout(log_group)
tab.setStyleSheet("background: transparent;")
layout = QVBoxLayout(tab)
layout.setSpacing(10)
layout.setContentsMargins(10, 10, 10, 10)
# Instructions log_status = QLabel("Watching chat log for skill gains...")
instr = QLabel("Hover over an ESI item and click scan") log_status.setStyleSheet("color: #4ecdc4;")
instr.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;") log_layout.addWidget(log_status)
layout.addWidget(instr)
# Scan ESI button self.gains_text = QTextEdit()
esi_btn = QPushButton("💉 Scan ESI Item") self.gains_text.setReadOnly(True)
esi_btn.setStyleSheet(""" self.gains_text.setMaximumHeight(150)
QPushButton { self.gains_text.setPlaceholderText("Recent skill gains will appear here...")
background-color: #9c27b0; log_layout.addWidget(self.gains_text)
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)
# ESI Results # Total gains summary
self.esi_result = QTextEdit() self.total_gains_label = QLabel(f"Total gains tracked: {len(self.skill_gains)}")
self.esi_result.setPlaceholderText("ESI scan results will appear here...") self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);")
self.esi_result.setStyleSheet(""" log_layout.addWidget(self.total_gains_label)
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)
layout.addStretch() splitter.addWidget(log_group)
return tab
def _create_data_tab(self): layout.addWidget(splitter)
"""Create data management tab."""
tab = QWidget()
tab.setStyleSheet("background: transparent;")
layout = QVBoxLayout(tab)
layout.setSpacing(10)
layout.setContentsMargins(10, 10, 10, 10)
# Stats # Load existing data
self.stats_label = QLabel("No data collected yet") self._refresh_skills_table()
self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 200);")
layout.addWidget(self.stats_label)
# Export button return widget
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
def _scan_skills(self): def _scan_skills(self):
"""Start skills scan.""" """Start OCR scan."""
self.status_label.setText("Capturing...") self.scanner = OCRScannerThread()
self.status_label.setStyleSheet("color: #4a9eff;") self.scanner.scan_complete.connect(self._on_scan_complete)
self.scanner.scan_error.connect(self._on_scan_error)
self.scan_thread = SkillScannerThread() self.scanner.progress_update.connect(self._on_scan_progress)
self.scan_thread.result_ready.connect(self._on_skills_scanned) self.scanner.start()
self.scan_thread.error_occurred.connect(self._on_scan_error)
self.scan_thread.progress_update.connect(self._on_scan_progress)
self.scan_thread.start()
def _on_scan_progress(self, message): def _on_scan_progress(self, message):
"""Update scan progress.""" """Update scan progress."""
self.status_label.setText(message) self.scan_progress.setText(message)
def _on_skills_scanned(self, skills_data): def _on_scan_complete(self, skills_data):
"""Handle scanned skills.""" """Handle scan completion."""
self.last_scan = skills_data self.skills_data.update(skills_data)
self._save_data()
# Save to data store self._refresh_skills_table()
self.data_store.save_skill_snapshot(skills_data) self.scan_progress.setText(f"Found {len(skills_data)} skills")
# 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_error(self, error): def _on_scan_error(self, error):
"""Handle scan error.""" """Handle scan error."""
self.status_label.setText(f"❌ Error: {error}") self.scan_progress.setText(f"Error: {error}")
self.status_label.setStyleSheet("color: #f44336;")
def _scan_esi(self): def _refresh_skills_table(self):
"""Scan ESI item.""" """Refresh skills table."""
self.esi_thread = ESIScannerThread() self.skills_table.setRowCount(len(self.skills_data))
self.esi_thread.result_ready.connect(self._on_esi_scanned) for i, (name, data) in enumerate(sorted(self.skills_data.items())):
self.esi_thread.error_occurred.connect(self._on_scan_error) self.skills_table.setItem(i, 0, QTableWidgetItem(name))
self.esi_thread.start() self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
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