feat: Resizable window, OCR scanners, customizable dashboard
This commit is contained in:
parent
5b127cf99e
commit
72c3c132ca
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Profession Scanner Plugin
|
||||
"""
|
||||
|
||||
from .plugin import ProfessionScannerPlugin
|
||||
|
||||
__all__ = ["ProfessionScannerPlugin"]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))))
|
||||
|
|
|
|||
Loading…
Reference in New Issue