Added automatic character stat page locator logic at startup.

This commit is contained in:
Nicole Rappe 2025-02-11 05:15:20 -07:00
parent 2812b96b7c
commit b72d1e4c92

View File

@ -3,7 +3,16 @@
import sys import sys
import time import time
import re import re
import numpy as np
import cv2
import pytesseract import pytesseract
try:
import winsound
HAS_WINSOUND = True
except ImportError:
HAS_WINSOUND = False
from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont from PyQt5.QtGui import QPainter, QPen, QColor, QFont
@ -15,31 +24,92 @@ from rich.progress import Progress, BarColumn, TextColumn
from rich.text import Text from rich.text import Text
from rich.live import Live from rich.live import Live
# ---- [ Global Config ] ---- # =============================================================================
# Global Config
# =============================================================================
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
OCR_ENGINE = "Tesseract"
POLLING_RATE_MS = 500 POLLING_RATE_MS = 500
MAX_DATA_POINTS = 8 MAX_DATA_POINTS = 8
DEFAULT_WIDTH = 175 # We still use these defaults for Region size.
DEFAULT_HEIGHT = 145 DEFAULT_WIDTH = 180
HANDLE_SIZE = 7 DEFAULT_HEIGHT = 130
HANDLE_SIZE = 8
LABEL_HEIGHT = 20 LABEL_HEIGHT = 20
GREEN_HEADER_STYLE = "bold green" GREEN_HEADER_STYLE = "bold green"
# ----------------------------------------------------------------------------- BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second
# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER)
# Set your theme to "Masquerade" and Interface Scale to 140%, and browser zoom level to 110%
TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file
MATCH_THRESHOLD = 0.5 # The correlation threshold to consider a "good" match
# =============================================================================
# Helper Functions # Helper Functions
# ----------------------------------------------------------------------------- # =============================================================================
def beep_hp_warning():
"""
Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS).
"""
current_time = time.time()
if (beep_hp_warning.last_beep_time is None or
(current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)):
beep_hp_warning.last_beep_time = current_time
if HAS_WINSOUND:
# frequency=376 Hz, duration=100 ms
winsound.Beep(376, 100)
else:
# Attempt terminal bell
print('\a', end='')
beep_hp_warning.last_beep_time = None
def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
"""
Attempt to locate the bars via OpenCV template matching:
1) Grab the full screen using PIL.ImageGrab.
2) Convert to NumPy array in BGR format for cv2.
3) Load template from `template_path`.
4) Use cv2.matchTemplate to find the best match location.
5) If max correlation > threshold, return (x, y, w, h).
6) Else return None.
"""
# 1) Capture full screen
screenshot_pil = ImageGrab.grab()
screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly
# Convert RGBA or RGB to BGR
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
# 2) Load template from file
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
if template_bgr is None:
print(f"[WARN] Could not load template file: {template_path}")
return None
# 3) Template matching
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
# 4) Find best match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# template width/height
th, tw, _ = template_bgr.shape
if max_val >= threshold:
# max_loc is top-left corner of the best match
found_x, found_y = max_loc
return (found_x, found_y, tw, th)
else:
return None
def format_duration(seconds): def format_duration(seconds):
"""
INFORMATION PROCESSING:
-----------------------
Convert total seconds into hours/min/seconds (e.g., "Xh Ym Zs").
Returns '???' if the input is None or invalid.
"""
if seconds is None: if seconds is None:
return "???" return "???"
seconds = int(seconds) seconds = int(seconds)
@ -52,13 +122,8 @@ def format_duration(seconds):
else: else:
return f"{mins}m {secs}s" return f"{mins}m {secs}s"
def sanitize_experience_string(raw_text): def sanitize_experience_string(raw_text):
"""
INFORMATION PROCESSING:
-----------------------
Extracts a float from raw OCR text for EXP (0-100%).
Handles e.g. "25.5682%", "77.8649" etc.
"""
text_no_percent = raw_text.replace('%', '') text_no_percent = raw_text.replace('%', '')
text_no_spaces = text_no_percent.replace(' ', '') text_no_spaces = text_no_percent.replace(' ', '')
cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces) cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
@ -72,12 +137,8 @@ def sanitize_experience_string(raw_text):
val = 100 val = 100
return round(val, 4) return round(val, 4)
def format_experience_value(value): def format_experience_value(value):
"""
INFORMATION DISPLAY (formatting):
---------------------------------
Format a float 0-100 to XX.XXXX for display in table output.
"""
if value < 0: if value < 0:
value = 0 value = 0
elif value > 100: elif value > 100:
@ -96,11 +157,8 @@ def format_experience_value(value):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Region Class # Region Class
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Region: class Region:
""" """
DATA STRUCTURE:
---------------
Defines a draggable/resizable screen region for OCR capture. Defines a draggable/resizable screen region for OCR capture.
""" """
def __init__(self, x, y, label="Region", color=QColor(0,0,255)): def __init__(self, x, y, label="Region", color=QColor(0,0,255)):
@ -120,30 +178,24 @@ class Region:
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT) return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
def resize_handles(self): def resize_handles(self):
"""
Return four small rectangles (handles) for resizing each corner.
"""
return [ return [
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # top-left QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # top-right QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # bottom-left QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # bottom-right QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
] ]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# OverlayCanvas Class # OverlayCanvas Class
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class OverlayCanvas(QWidget): class OverlayCanvas(QWidget):
""" """
UI RENDERING LOGIC:
-------------------
Renders the overlay & handles region dragging/resizing. Renders the overlay & handles region dragging/resizing.
""" """
def __init__(self, regions, parent=None): def __init__(self, regions, parent=None):
super().__init__(parent) super().__init__(parent)
self.regions = regions self.regions = regions
self.edit_mode = True # allow editing by default self.edit_mode = True
self.selected_region = None self.selected_region = None
self.selected_handle = None self.selected_handle = None
self.drag_offset = QPoint() self.drag_offset = QPoint()
@ -152,18 +204,15 @@ class OverlayCanvas(QWidget):
painter = QPainter(self) painter = QPainter(self)
for region in self.regions: for region in self.regions:
if region.visible: if region.visible:
# Draw the bounding rectangle
pen = QPen(region.color) pen = QPen(region.color)
pen.setWidth(3) pen.setWidth(3)
painter.setPen(pen) painter.setPen(pen)
painter.drawRect(region.x, region.y, region.w, region.h) painter.drawRect(region.x, region.y, region.w, region.h)
# Draw the region label
painter.setFont(QFont("Arial", 12, QFont.Bold)) painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(region.color) painter.setPen(region.color)
painter.drawText(region.x, region.y - 5, region.label) painter.drawText(region.x, region.y - 5, region.label)
# If in edit mode, show corner handles
if self.edit_mode: if self.edit_mode:
for handle in region.resize_handles(): for handle in region.resize_handles():
painter.fillRect(handle, region.color) painter.fillRect(handle, region.color)
@ -171,17 +220,13 @@ class OverlayCanvas(QWidget):
def mousePressEvent(self, event): def mousePressEvent(self, event):
if not self.edit_mode: if not self.edit_mode:
return return
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
# Check topmost region first
for region in reversed(self.regions): for region in reversed(self.regions):
# Check each resize handle
for i, handle in enumerate(region.resize_handles()): for i, handle in enumerate(region.resize_handles()):
if handle.contains(event.pos()): if handle.contains(event.pos()):
self.selected_region = region self.selected_region = region
self.selected_handle = i self.selected_handle = i
return return
# Check label or main rect for dragging
if region.label_rect().contains(event.pos()): if region.label_rect().contains(event.pos()):
self.selected_region = region self.selected_region = region
self.selected_handle = None self.selected_handle = None
@ -198,11 +243,9 @@ class OverlayCanvas(QWidget):
return return
if self.selected_handle is None: if self.selected_handle is None:
# Drag entire rectangle
self.selected_region.x = event.x() - self.drag_offset.x() self.selected_region.x = event.x() - self.drag_offset.x()
self.selected_region.y = event.y() - self.drag_offset.y() self.selected_region.y = event.y() - self.drag_offset.y()
else: else:
# Resize
sr = self.selected_region sr = self.selected_region
if self.selected_handle == 0: # top-left if self.selected_handle == 0: # top-left
sr.w += sr.x - event.x() sr.w += sr.x - event.x()
@ -221,11 +264,10 @@ class OverlayCanvas(QWidget):
sr.w = event.x() - sr.x sr.w = event.x() - sr.x
sr.h = event.y() - sr.y sr.h = event.y() - sr.y
# Enforce min size
sr.w = max(sr.w, 10) sr.w = max(sr.w, 10)
sr.h = max(sr.h, 10) sr.h = max(sr.h, 10)
self.update() # repaint self.update()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if not self.edit_mode: if not self.edit_mode:
@ -237,12 +279,13 @@ class OverlayCanvas(QWidget):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# BorealisOverlay Class # BorealisOverlay Class
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class BorealisOverlay(QWidget): class BorealisOverlay(QWidget):
""" """
MAIN APPLICATION LOGIC: Single Region Overlay for Player Stats (HP/MP/FP/EXP) with:
----------------------- - Automatic location via OpenCV template matching at startup
Single Region Overlay for Player Stats (HP/MP/FP/EXP) with OCR scanning. - OCR scanning
- Low-HP beep
- Rich Live updates in terminal
""" """
def __init__(self, live=None): def __init__(self, live=None):
super().__init__() super().__init__()
@ -251,9 +294,24 @@ class BorealisOverlay(QWidget):
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True) self.setAttribute(Qt.WA_TranslucentBackground, True)
# Single region, with an increased height (120) # Try to find the bars automatically
region = Region(250, 50, label="Character Status") # If found => use that location, else default
region.h = 120 initial_x, initial_y = 250, 50
region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT
match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD)
if match_result is not None:
found_x, found_y, w, h = match_result
print(f"Template matched at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.")
initial_x, initial_y = found_x, found_y
# Optionally override region size with template size
region_w, region_h = w, h
else:
print("No high-confidence match found. Using default region.")
region = Region(initial_x, initial_y, label="Character Status")
region.w = region_w
region.h = region_h
self.regions = [region] self.regions = [region]
self.canvas = OverlayCanvas(self.regions, self) self.canvas = OverlayCanvas(self.regions, self)
@ -265,7 +323,6 @@ class BorealisOverlay(QWidget):
# Keep history of EXP data # Keep history of EXP data
self.points = [] self.points = []
# We will store a reference to Rich.Live here
self.live = live self.live = live
# Timer for periodic OCR scanning # Timer for periodic OCR scanning
@ -274,121 +331,69 @@ class BorealisOverlay(QWidget):
self.timer.start(POLLING_RATE_MS) self.timer.start(POLLING_RATE_MS)
def set_live(self, live): def set_live(self, live):
"""
Called by main() so we can update the Live object from inside this class.
"""
self.live = live self.live = live
# -------------------------------------------------------------------------
# OCR
# -------------------------------------------------------------------------
def collect_ocr_data(self): def collect_ocr_data(self):
"""
INFORMATION GATHERING:
----------------------
Periodically invoked by QTimer. Captures region screenshot, OCR's it,
and triggers the terminal display update.
"""
for region in self.regions: for region in self.regions:
if region.visible: if region.visible:
screenshot = ImageGrab.grab( screenshot = ImageGrab.grab(
bbox=(region.x, region.y, region.x + region.w, region.y + region.h) bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
) )
processed = self.preprocess_image(screenshot) processed = self.preprocess_image(screenshot)
# Use psm=4 for multi-line
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1') text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
region.data = text.strip() region.data = text.strip()
# Instead of printing directly, we now build a Rich renderable and update Live.
if self.live is not None: if self.live is not None:
renderable = self.build_renderable() renderable = self.build_renderable()
self.live.update(renderable) self.live.update(renderable)
def preprocess_image(self, image): def preprocess_image(self, image):
"""
INFORMATION PROCESSING:
-----------------------
Convert to grayscale, scale up, threshold, median filter
for improved Tesseract accuracy.
"""
gray = image.convert("L") gray = image.convert("L")
scaled = gray.resize((gray.width * 3, gray.height * 3)) scaled = gray.resize((gray.width * 3, gray.height * 3))
thresh = scaled.point(lambda p: p > 200 and 255) thresh = scaled.point(lambda p: p > 200 and 255)
return thresh.filter(ImageFilter.MedianFilter(3)) return thresh.filter(ImageFilter.MedianFilter(3))
# -------------------------------------------------------------------------
# Parsing
# -------------------------------------------------------------------------
def parse_all_stats(self, raw_text): def parse_all_stats(self, raw_text):
"""
INFORMATION ANALYSIS:
---------------------
Expect up to 4 lines: HP, MP, FP, EXP.
Returns dict with keys "hp", "mp", "fp", "exp".
"""
raw_lines = raw_text.splitlines() raw_lines = raw_text.splitlines()
lines = [l.strip() for l in raw_lines if l.strip()] # remove empty lines lines = [l.strip() for l in raw_lines if l.strip()]
stats_dict = { stats_dict = {
"hp": (0, 1), "hp": (0,1),
"mp": (0, 1), "mp": (0,1),
"fp": (0, 1), "fp": (0,1),
"exp": None "exp": None
} }
if len(lines) < 4: if len(lines) < 4:
return stats_dict return stats_dict
# HP
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
if hp_match: if hp_match:
stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2))) stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))
# MP
mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1]) mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
if mp_match: if mp_match:
stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2))) stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))
# FP
fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2]) fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
if fp_match: if fp_match:
stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2))) stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2)))
# EXP
exp_val = sanitize_experience_string(lines[3]) exp_val = sanitize_experience_string(lines[3])
stats_dict["exp"] = exp_val stats_dict["exp"] = exp_val
return stats_dict return stats_dict
def update_points(self, new_val): def update_points(self, new_val):
"""
INFORMATION TRACKING:
---------------------
Track historical EXP changes for table & predicted time to level.
"""
now = time.time() now = time.time()
if self.points: if self.points:
_, last_v = self.points[-1] _, last_v = self.points[-1]
# skip duplicates
if abs(new_val - last_v) < 1e-6: if abs(new_val - last_v) < 1e-6:
return return
# if new_val < last_v, assume rollover
if new_val < last_v: if new_val < last_v:
self.points.clear() self.points.clear()
self.points.append((now, new_val)) self.points.append((now, new_val))
if len(self.points) > MAX_DATA_POINTS: if len(self.points) > MAX_DATA_POINTS:
self.points.pop(0) self.points.pop(0)
# -------------------------------------------------------------------------
# Display Logic
# -------------------------------------------------------------------------
def compute_time_to_100(self): def compute_time_to_100(self):
"""
INFORMATION PREDICTION:
-----------------------
Estimate time to reach 100% from current EXP data.
"""
n = len(self.points) n = len(self.points)
if n < 2: if n < 2:
return None return None
@ -416,16 +421,6 @@ class BorealisOverlay(QWidget):
return int(remain / rate_per_s) return int(remain / rate_per_s)
def build_renderable(self): def build_renderable(self):
"""
INFORMATION DISPLAY (Rich):
---------------------------
Construct a single Rich renderable (Group) that includes:
- Title
- HP/MP/FP progress bars
- Historical EXP table
- Predicted time progress bar
"""
# Gather stats from first region
raw_text = self.regions[0].data raw_text = self.regions[0].data
stats = self.parse_all_stats(raw_text) stats = self.parse_all_stats(raw_text)
hp_cur, hp_max = stats["hp"] hp_cur, hp_max = stats["hp"]
@ -433,57 +428,37 @@ class BorealisOverlay(QWidget):
fp_cur, fp_max = stats["fp"] fp_cur, fp_max = stats["fp"]
exp_val = stats["exp"] exp_val = stats["exp"]
# Update historical EXP points if valid # HP beep logic
if hp_max > 0:
hp_ratio = hp_cur / hp_max
if 0 < hp_ratio <= 0.40:
beep_hp_warning()
if exp_val is not None: if exp_val is not None:
self.update_points(exp_val) self.update_points(exp_val)
current_exp = self.points[-1][1] if self.points else 0.0 current_exp = self.points[-1][1] if self.points else 0.0
# --------------------- # Title
# 1) Title Section
# ---------------------
title_text = Text("Project Borealis\n", style="bold white") title_text = Text("Project Borealis\n", style="bold white")
subtitle_text = Text("Flyff Information Overlay\n\n", style="dim") subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")
# --------------------- # HP / MP / FP bars
# 2) HP / MP / FP Bars
# ---------------------
bar_progress = Progress( bar_progress = Progress(
"{task.description}", "{task.description}",
BarColumn(bar_width=30), BarColumn(bar_width=30),
TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"), TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
transient=False, transient=False,
auto_refresh=False # We'll refresh after all tasks are added auto_refresh=False
)
# HP
hp_task = bar_progress.add_task(
"[bold red]HP[/bold red]",
total=hp_max,
completed=hp_cur,
style="red",
complete_style="red"
)
# MP
mp_task = bar_progress.add_task(
"[bold blue]MP[/bold blue]",
total=mp_max,
completed=mp_cur,
style="blue",
complete_style="blue"
)
# FP
fp_task = bar_progress.add_task(
"[bold green]FP[/bold green]",
total=fp_max,
completed=fp_cur,
style="green",
complete_style="green"
) )
bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur,
style="red", complete_style="red")
bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur,
style="blue", complete_style="blue")
bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur,
style="green", complete_style="green")
bar_progress.refresh() bar_progress.refresh()
# --------------------- # Historical EXP table
# 3) Historical EXP Table
# ---------------------
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None) table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
table.add_column("Historical EXP", justify="center", style="green") table.add_column("Historical EXP", justify="center", style="green")
table.add_column("Time Since Last Kill", justify="center", style="green") table.add_column("Time Since Last Kill", justify="center", style="green")
@ -503,7 +478,6 @@ class BorealisOverlay(QWidget):
t_prev, v_prev = self.points[i - 1] t_prev, v_prev = self.points[i - 1]
delta_v = v_cur - v_prev delta_v = v_cur - v_prev
delta_str = f"{delta_v:+.4f}%" delta_str = f"{delta_v:+.4f}%"
exp_main = format_experience_value(v_cur) exp_main = format_experience_value(v_cur)
exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]" exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]"
@ -520,9 +494,7 @@ class BorealisOverlay(QWidget):
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str) table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
# --------------------- # Predicted Time to Level
# 4) Predicted Time to Level
# ---------------------
secs_left = self.compute_time_to_100() secs_left = self.compute_time_to_100()
time_str = format_duration(secs_left) time_str = format_duration(secs_left)
@ -534,45 +506,35 @@ class BorealisOverlay(QWidget):
transient=False, transient=False,
auto_refresh=False auto_refresh=False
) )
t_task = time_bar.add_task("", total=100, completed=current_exp) time_bar.add_task("", total=100, completed=current_exp)
time_bar.refresh() time_bar.refresh()
# Combine everything into a Rich Group
# Title + Subtitle + HP/MP/FP Progress + Table + Time Bar
return Group( return Group(
title_text, title_text,
subtitle_text, subtitle_text,
bar_progress, # HP/MP/FP bar_progress,
table, table,
time_bar # predicted-time progress time_bar
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# main() # main
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
""" """
LAUNCH SEQUENCE: 1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching.
--------------- 2) Position overlay region accordingly if found, else default.
1) Create QApplication. 3) Start PyQt, periodically OCR the region, update Rich Live in terminal.
2) Create BorealisOverlay Window.
3) Use Rich Live to continuously update terminal output with no flicker.
4) Start PyQt event loop.
""" """
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = BorealisOverlay() # We'll inject Live momentarily window = BorealisOverlay()
window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)") window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
window.show() window.show()
console = Console() console = Console()
# Use a Live context manager so we can do partial updates
with Live(console=console, refresh_per_second=4) as live: with Live(console=console, refresh_per_second=4) as live:
# Pass the live object to our BorealisOverlay so it can call live.update()
window.set_live(live) window.set_live(live)
# Run the PyQt event loop (blocking)
exit_code = app.exec_() exit_code = app.exec_()
sys.exit(exit_code) sys.exit(exit_code)