From 72c3c132ca5a6f33aa918f8ba810ae9305e97004 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Fri, 13 Feb 2026 17:12:58 +0000 Subject: [PATCH] feat: Resizable window, OCR scanners, customizable dashboard --- projects/EU-Utility/core/overlay_window.py | 19 +- .../EU-Utility/plugins/dashboard/plugin.py | 430 +++++---- .../plugins/profession_scanner/__init__.py | 7 + .../plugins/profession_scanner/plugin.py | 247 +++++ .../plugins/skill_scanner/plugin.py | 844 ++++++------------ 5 files changed, 778 insertions(+), 769 deletions(-) create mode 100644 projects/EU-Utility/plugins/profession_scanner/__init__.py create mode 100644 projects/EU-Utility/plugins/profession_scanner/plugin.py diff --git a/projects/EU-Utility/core/overlay_window.py b/projects/EU-Utility/core/overlay_window.py index 01738d4..e6b332e 100644 --- a/projects/EU-Utility/core/overlay_window.py +++ b/projects/EU-Utility/core/overlay_window.py @@ -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: diff --git a/projects/EU-Utility/plugins/dashboard/plugin.py b/projects/EU-Utility/plugins/dashboard/plugin.py index ccea064..47f5ef1 100644 --- a/projects/EU-Utility/plugins/dashboard/plugin.py +++ b/projects/EU-Utility/plugins/dashboard/plugin.py @@ -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() diff --git a/projects/EU-Utility/plugins/profession_scanner/__init__.py b/projects/EU-Utility/plugins/profession_scanner/__init__.py new file mode 100644 index 0000000..7248c5f --- /dev/null +++ b/projects/EU-Utility/plugins/profession_scanner/__init__.py @@ -0,0 +1,7 @@ +""" +Profession Scanner Plugin +""" + +from .plugin import ProfessionScannerPlugin + +__all__ = ["ProfessionScannerPlugin"] diff --git a/projects/EU-Utility/plugins/profession_scanner/plugin.py b/projects/EU-Utility/plugins/profession_scanner/plugin.py new file mode 100644 index 0000000..c35ad5c --- /dev/null +++ b/projects/EU-Utility/plugins/profession_scanner/plugin.py @@ -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) diff --git a/projects/EU-Utility/plugins/skill_scanner/plugin.py b/projects/EU-Utility/plugins/skill_scanner/plugin.py index 93c4b2d..5fd14a8 100644 --- a/projects/EU-Utility/plugins/skill_scanner/plugin.py +++ b/projects/EU-Utility/plugins/skill_scanner/plugin.py @@ -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 save_skill_snapshot(self, skills_data): - """Save a skill snapshot with timestamp.""" - snapshot = { - "timestamp": datetime.now().isoformat(), - "skills": skills_data - } + def __init__(self, log_path, parent=None): + super().__init__(parent) + self.log_path = Path(log_path) + self.running = True + self.last_position = 0 - # 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 - - 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) + splitter.addWidget(scan_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 tracking section + log_group = QGroupBox("Automatic Log Tracking") + log_layout = QVBoxLayout(log_group) - # 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) + log_status = QLabel("Watching chat log for skill gains...") + log_status.setStyleSheet("color: #4ecdc4;") + log_layout.addWidget(log_status) - # 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) + 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) - layout.addStretch() - return tab - - 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) + # 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) - # Stats - self.stats_label = QLabel("No data collected yet") - self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 200);") - layout.addWidget(self.stats_label) + splitter.addWidget(log_group) - # 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) + layout.addWidget(splitter) - # 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) + # Load existing data + self._refresh_skills_table() - 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))))