Adjusted default region dimensions

This commit is contained in:
Nicole Rappe 2025-02-11 03:25:26 -07:00
parent 01da38d05d
commit 2812b96b7c

View File

@ -1,23 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Project Borealis (Single Region: HP/MP/FP/EXP)
==============================================
One region labeled "Player Stats" capturing 4 lines:
1) HP: current / max
2) MP: current / max
3) FP: current / max
4) EXP (percentage)
HP, MP, FP each have Rich progress bars that stay their assigned color at 100%.
The 4th line (EXP) feeds into historical EXP logic (table + predicted time).
Region is resizable & draggable (edit_mode = True).
Tesseract uses --psm 4 for multi-line segmentation.
Adjust region.x, region.y, region.w, region.h to match your UI.
"""
import sys import sys
import os
import time import time
import re import re
import pytesseract import pytesseract
@ -26,28 +9,36 @@ from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont from PyQt5.QtGui import QPainter, QPen, QColor, QFont
from PIL import Image, ImageGrab, ImageFilter from PIL import Image, ImageGrab, ImageFilter
from rich.console import Console from rich.console import Console, Group
from rich.table import Table from rich.table import Table
from rich.progress import Progress, BarColumn, TextColumn from rich.progress import Progress, BarColumn, TextColumn
from rich.text import Text
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" OCR_ENGINE = "Tesseract"
POLLING_RATE_MS = 1000 POLLING_RATE_MS = 500
MAX_DATA_POINTS = 7 MAX_DATA_POINTS = 8
DEFAULT_WIDTH = 150 DEFAULT_WIDTH = 175
DEFAULT_HEIGHT = 120 # taller to ensure line 4 is captured DEFAULT_HEIGHT = 145
HANDLE_SIZE = 10 HANDLE_SIZE = 7
LABEL_HEIGHT = 20 LABEL_HEIGHT = 20
GREEN_HEADER_STYLE = "bold green" GREEN_HEADER_STYLE = "bold green"
# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------
def format_duration(seconds): def format_duration(seconds):
""" """
INFORMATION PROCESSING:
-----------------------
Convert total seconds into hours/min/seconds (e.g., "Xh Ym Zs"). Convert total seconds into hours/min/seconds (e.g., "Xh Ym Zs").
Returns '???' if None. Returns '???' if the input is None or invalid.
""" """
if seconds is None: if seconds is None:
return "???" return "???"
@ -63,6 +54,8 @@ def format_duration(seconds):
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%). Extracts a float from raw OCR text for EXP (0-100%).
Handles e.g. "25.5682%", "77.8649" etc. Handles e.g. "25.5682%", "77.8649" etc.
""" """
@ -81,6 +74,8 @@ def sanitize_experience_string(raw_text):
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. Format a float 0-100 to XX.XXXX for display in table output.
""" """
if value < 0: if value < 0:
@ -98,8 +93,14 @@ def format_experience_value(value):
int_part = "00" int_part = "00"
return f"{int_part}.{dec_part}" return f"{int_part}.{dec_part}"
# -----------------------------------------------------------------------------
# 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)):
@ -129,8 +130,14 @@ class Region:
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), # bottom-right
] ]
# -----------------------------------------------------------------------------
# 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):
@ -166,7 +173,7 @@ class OverlayCanvas(QWidget):
return return
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
# Check topmost region first (reverse if multiple) # Check topmost region first
for region in reversed(self.regions): for region in reversed(self.regions):
# Check each resize handle # Check each resize handle
for i, handle in enumerate(region.resize_handles()): for i, handle in enumerate(region.resize_handles()):
@ -227,11 +234,17 @@ class OverlayCanvas(QWidget):
self.selected_region = None self.selected_region = None
self.selected_handle = None self.selected_handle = None
# -----------------------------------------------------------------------------
# BorealisOverlay Class
# -----------------------------------------------------------------------------
class BorealisOverlay(QWidget): class BorealisOverlay(QWidget):
""" """
Single Region Overlay for Player Stats (HP/MP/FP/EXP) MAIN APPLICATION LOGIC:
-----------------------
Single Region Overlay for Player Stats (HP/MP/FP/EXP) with OCR scanning.
""" """
def __init__(self): def __init__(self, live=None):
super().__init__() super().__init__()
screen_geo = QApplication.primaryScreen().geometry() screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo) self.setGeometry(screen_geo)
@ -239,7 +252,7 @@ class BorealisOverlay(QWidget):
self.setAttribute(Qt.WA_TranslucentBackground, True) self.setAttribute(Qt.WA_TranslucentBackground, True)
# Single region, with an increased height (120) # Single region, with an increased height (120)
region = Region(250, 50, label="Player Stats") region = Region(250, 50, label="Character Status")
region.h = 120 region.h = 120
self.regions = [region] self.regions = [region]
@ -252,15 +265,30 @@ 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
# Timer for periodic OCR scanning # Timer for periodic OCR scanning
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.timeout.connect(self.collect_ocr_data) self.timer.timeout.connect(self.collect_ocr_data)
self.timer.start(POLLING_RATE_MS) self.timer.start(POLLING_RATE_MS)
# --------------------------------------------------------------------- def set_live(self, live):
"""
Called by main() so we can update the Live object from inside this class.
"""
self.live = live
# -------------------------------------------------------------------------
# OCR # 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(
@ -272,10 +300,15 @@ class BorealisOverlay(QWidget):
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()
self.display_ocr_data_in_terminal() # Instead of printing directly, we now build a Rich renderable and update Live.
if self.live is not None:
renderable = self.build_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 Convert to grayscale, scale up, threshold, median filter
for improved Tesseract accuracy. for improved Tesseract accuracy.
""" """
@ -284,11 +317,13 @@ class BorealisOverlay(QWidget):
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 # 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. Expect up to 4 lines: HP, MP, FP, EXP.
Returns dict with keys "hp", "mp", "fp", "exp". Returns dict with keys "hp", "mp", "fp", "exp".
""" """
@ -328,6 +363,8 @@ class BorealisOverlay(QWidget):
def update_points(self, new_val): def update_points(self, new_val):
""" """
INFORMATION TRACKING:
---------------------
Track historical EXP changes for table & predicted time to level. Track historical EXP changes for table & predicted time to level.
""" """
now = time.time() now = time.time()
@ -343,11 +380,13 @@ class BorealisOverlay(QWidget):
if len(self.points) > MAX_DATA_POINTS: if len(self.points) > MAX_DATA_POINTS:
self.points.pop(0) self.points.pop(0)
# --------------------------------------------------------------------- # -------------------------------------------------------------------------
# Display # 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. Estimate time to reach 100% from current EXP data.
""" """
n = len(self.points) n = len(self.points)
@ -376,18 +415,17 @@ class BorealisOverlay(QWidget):
return int(remain / rate_per_s) return int(remain / rate_per_s)
def display_ocr_data_in_terminal(self): def build_renderable(self):
""" """
Clears terminal, prints HP/MP/FP bars, EXP table, predicted time. INFORMATION DISPLAY (Rich):
---------------------------
Construct a single Rich renderable (Group) that includes:
- Title
- HP/MP/FP progress bars
- Historical EXP table
- Predicted time progress bar
""" """
console = Console() # Gather stats from first region
os.system('cls' if os.name == 'nt' else 'clear')
# Title
console.print("[bold white]Project Borealis[/bold white]")
console.print("[dim]Flyff Information Overlay[/dim]\n")
# Parse stats
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"]
@ -395,48 +433,57 @@ class BorealisOverlay(QWidget):
fp_cur, fp_max = stats["fp"] fp_cur, fp_max = stats["fp"]
exp_val = stats["exp"] exp_val = stats["exp"]
# Show HP / MP / FP bars, forcing each color to remain even at 100% # Update historical EXP points if valid
with Progress( if exp_val is not None:
self.update_points(exp_val)
current_exp = self.points[-1][1] if self.points else 0.0
# ---------------------
# 1) Title Section
# ---------------------
title_text = Text("Project Borealis\n", style="bold white")
subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")
# ---------------------
# 2) HP / MP / FP Bars
# ---------------------
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}%)"),
console=console,
transient=False, transient=False,
auto_refresh=True, auto_refresh=False # We'll refresh after all tasks are added
) as progress: )
progress.add_task( # HP
"[bold red]HP[/bold red]", hp_task = bar_progress.add_task(
total=hp_max, "[bold red]HP[/bold red]",
completed=hp_cur, total=hp_max,
style="red", completed=hp_cur,
complete_style="red" # remain red at 100% style="red",
) complete_style="red"
progress.add_task( )
"[bold blue]MP[/bold blue]", # MP
total=mp_max, mp_task = bar_progress.add_task(
completed=mp_cur, "[bold blue]MP[/bold blue]",
style="blue", total=mp_max,
complete_style="blue" # remain blue at 100% completed=mp_cur,
) style="blue",
progress.add_task( complete_style="blue"
"[bold green]FP[/bold green]", )
total=fp_max, # FP
completed=fp_cur, fp_task = bar_progress.add_task(
style="green", "[bold green]FP[/bold green]",
complete_style="green" # remain green at 100% total=fp_max,
) completed=fp_cur,
style="green",
complete_style="green"
)
bar_progress.refresh()
progress.refresh() # ---------------------
progress.stop() # 3) Historical EXP Table
# ---------------------
console.print() # blank line after bars
# If we have an EXP value, update historical data
if exp_val is not None:
self.update_points(exp_val)
# Build the 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")
@ -454,9 +501,9 @@ class BorealisOverlay(QWidget):
for i in range(1, n): for i in range(1, n):
t_cur, v_cur = self.points[i] t_cur, v_cur = self.points[i]
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]"
@ -473,34 +520,62 @@ 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)
console.print(table) # ---------------------
console.print() # blank line # 4) Predicted Time to Level
# ---------------------
# Predicted time to level
current_exp = self.points[-1][1] if self.points else 0.0
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)
with Progress( time_bar = Progress(
TextColumn("[bold white]Predicted Time to Level:[/bold white] "), TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
BarColumn(bar_width=30, complete_style="magenta"), BarColumn(bar_width=30, complete_style="magenta"),
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "), TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"), TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
console=console,
transient=False, transient=False,
) as progress: auto_refresh=False
progress.add_task("", total=100, completed=current_exp) )
progress.refresh() t_task = time_bar.add_task("", total=100, completed=current_exp)
time_bar.refresh()
# Combine everything into a Rich Group
# Title + Subtitle + HP/MP/FP Progress + Table + Time Bar
return Group(
title_text,
subtitle_text,
bar_progress, # HP/MP/FP
table,
time_bar # predicted-time progress
)
# -----------------------------------------------------------------------------
# main()
# -----------------------------------------------------------------------------
def main(): def main():
""" """
Launches the PyQt5 overlay, starts the event loop. LAUNCH SEQUENCE:
---------------
1) Create QApplication.
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() window = BorealisOverlay() # We'll inject Live momentarily
window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)") window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
window.show() window.show()
sys.exit(app.exec_())
console = Console()
# Use a Live context manager so we can do partial updates
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)
# Run the PyQt event loop (blocking)
exit_code = app.exec_()
sys.exit(exit_code)
if __name__ == "__main__": if __name__ == "__main__":
main() main()