From 2812b96b7ccd82450748b78e12cf6cee2d05f9d3 Mon Sep 17 00:00:00 2001
From: Nicole Rappe <nicole.rappe@bunny-lab.io>
Date: Tue, 11 Feb 2025 03:25:26 -0700
Subject: [PATCH] Adjusted default region dimensions

---
 borealis_overlay.py | 269 ++++++++++++++++++++++++++++----------------
 1 file changed, 172 insertions(+), 97 deletions(-)

diff --git a/borealis_overlay.py b/borealis_overlay.py
index 4a496c5..3a3a3b1 100644
--- a/borealis_overlay.py
+++ b/borealis_overlay.py
@@ -1,23 +1,6 @@
 #!/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 os
 import time
 import re
 import pytesseract
@@ -26,28 +9,36 @@ from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
 from PyQt5.QtGui import QPainter, QPen, QColor, QFont
 from PIL import Image, ImageGrab, ImageFilter
 
-from rich.console import Console
+from rich.console import Console, Group
 from rich.table import Table
 from rich.progress import Progress, BarColumn, TextColumn
+from rich.text import Text
+from rich.live import Live
 
 # ---- [ Global Config ] ----
 pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
 
-OCR_ENGINE = "Tesseract"  
-POLLING_RATE_MS = 1000    
-MAX_DATA_POINTS = 7
+OCR_ENGINE = "Tesseract"
+POLLING_RATE_MS = 500
+MAX_DATA_POINTS = 8
 
-DEFAULT_WIDTH = 150
-DEFAULT_HEIGHT = 120  # taller to ensure line 4 is captured
-HANDLE_SIZE = 10
+DEFAULT_WIDTH = 175
+DEFAULT_HEIGHT = 145
+HANDLE_SIZE = 7
 LABEL_HEIGHT = 20
 
 GREEN_HEADER_STYLE = "bold green"
 
+# -----------------------------------------------------------------------------
+#                           Helper Functions
+# -----------------------------------------------------------------------------
+
 def format_duration(seconds):
     """
+    INFORMATION PROCESSING:
+    -----------------------
     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:
         return "???"
@@ -63,6 +54,8 @@ def format_duration(seconds):
 
 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.
     """
@@ -81,6 +74,8 @@ def sanitize_experience_string(raw_text):
 
 def format_experience_value(value):
     """
+    INFORMATION DISPLAY (formatting):
+    ---------------------------------
     Format a float 0-100 to XX.XXXX for display in table output.
     """
     if value < 0:
@@ -98,8 +93,14 @@ def format_experience_value(value):
         int_part = "00"
     return f"{int_part}.{dec_part}"
 
+# -----------------------------------------------------------------------------
+#                              Region Class
+# -----------------------------------------------------------------------------
+
 class Region:
     """
+    DATA STRUCTURE:
+    ---------------
     Defines a draggable/resizable screen region for OCR capture.
     """
     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
         ]
 
+# -----------------------------------------------------------------------------
+#                            OverlayCanvas Class
+# -----------------------------------------------------------------------------
+
 class OverlayCanvas(QWidget):
     """
+    UI RENDERING LOGIC:
+    -------------------
     Renders the overlay & handles region dragging/resizing.
     """
     def __init__(self, regions, parent=None):
@@ -166,7 +173,7 @@ class OverlayCanvas(QWidget):
             return
 
         if event.button() == Qt.LeftButton:
-            # Check topmost region first (reverse if multiple)
+            # Check topmost region first
             for region in reversed(self.regions):
                 # Check each resize handle
                 for i, handle in enumerate(region.resize_handles()):
@@ -227,11 +234,17 @@ class OverlayCanvas(QWidget):
             self.selected_region = None
             self.selected_handle = None
 
+# -----------------------------------------------------------------------------
+#                           BorealisOverlay Class
+# -----------------------------------------------------------------------------
+
 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__()
         screen_geo = QApplication.primaryScreen().geometry()
         self.setGeometry(screen_geo)
@@ -239,7 +252,7 @@ class BorealisOverlay(QWidget):
         self.setAttribute(Qt.WA_TranslucentBackground, True)
 
         # Single region, with an increased height (120)
-        region = Region(250, 50, label="Player Stats")
+        region = Region(250, 50, label="Character Status")
         region.h = 120
         self.regions = [region]
 
@@ -252,15 +265,30 @@ class BorealisOverlay(QWidget):
         # Keep history of EXP data
         self.points = []
 
+        # We will store a reference to Rich.Live here
+        self.live = live
+
         # Timer for periodic OCR scanning
         self.timer = QTimer(self)
         self.timer.timeout.connect(self.collect_ocr_data)
         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
-    # ---------------------------------------------------------------------
+    # -------------------------------------------------------------------------
     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:
             if region.visible:
                 screenshot = ImageGrab.grab(
@@ -272,10 +300,15 @@ class BorealisOverlay(QWidget):
                 text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
                 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):
         """
+        INFORMATION PROCESSING:
+        -----------------------
         Convert to grayscale, scale up, threshold, median filter
         for improved Tesseract accuracy.
         """
@@ -284,11 +317,13 @@ class BorealisOverlay(QWidget):
         thresh = scaled.point(lambda p: p > 200 and 255)
         return thresh.filter(ImageFilter.MedianFilter(3))
 
-    # ---------------------------------------------------------------------
+    # -------------------------------------------------------------------------
     #                           Parsing
-    # ---------------------------------------------------------------------
+    # -------------------------------------------------------------------------
     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".
         """
@@ -328,6 +363,8 @@ class BorealisOverlay(QWidget):
 
     def update_points(self, new_val):
         """
+        INFORMATION TRACKING:
+        ---------------------
         Track historical EXP changes for table & predicted time to level.
         """
         now = time.time()
@@ -343,11 +380,13 @@ class BorealisOverlay(QWidget):
         if len(self.points) > MAX_DATA_POINTS:
             self.points.pop(0)
 
-    # ---------------------------------------------------------------------
-    #                           Display
-    # ---------------------------------------------------------------------
+    # -------------------------------------------------------------------------
+    #                           Display Logic
+    # -------------------------------------------------------------------------
     def compute_time_to_100(self):
         """
+        INFORMATION PREDICTION:
+        -----------------------
         Estimate time to reach 100% from current EXP data.
         """
         n = len(self.points)
@@ -376,18 +415,17 @@ class BorealisOverlay(QWidget):
 
         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()
-        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
+        # Gather stats from first region
         raw_text = self.regions[0].data
         stats = self.parse_all_stats(raw_text)
         hp_cur, hp_max = stats["hp"]
@@ -395,48 +433,57 @@ class BorealisOverlay(QWidget):
         fp_cur, fp_max = stats["fp"]
         exp_val = stats["exp"]
 
-        # Show HP / MP / FP bars, forcing each color to remain even at 100%
-        with Progress(
+        # Update historical EXP points if valid
+        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}",
             BarColumn(bar_width=30),
             TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
-            console=console,
             transient=False,
-            auto_refresh=True,
-        ) as progress:
+            auto_refresh=False  # We'll refresh after all tasks are added
+        )
 
-            progress.add_task(
-                "[bold red]HP[/bold red]",
-                total=hp_max,
-                completed=hp_cur,
-                style="red",
-                complete_style="red"    # remain red at 100%
-            )
-            progress.add_task(
-                "[bold blue]MP[/bold blue]",
-                total=mp_max,
-                completed=mp_cur,
-                style="blue",
-                complete_style="blue"   # remain blue at 100%
-            )
-            progress.add_task(
-                "[bold green]FP[/bold green]",
-                total=fp_max,
-                completed=fp_cur,
-                style="green",
-                complete_style="green"  # remain green at 100%
-            )
+        # 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.refresh()
 
-            progress.refresh()
-            progress.stop()
-
-        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
+        # ---------------------
+        # 3) Historical EXP Table
+        # ---------------------
         table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
         table.add_column("Historical EXP", 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):
                 t_cur, v_cur = self.points[i]
                 t_prev, v_prev = self.points[i - 1]
-
                 delta_v = v_cur - v_prev
                 delta_str = f"{delta_v:+.4f}%"
+
                 exp_main = format_experience_value(v_cur)
                 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)
 
-        console.print(table)
-        console.print()  # blank line
-
-        # Predicted time to level
-        current_exp = self.points[-1][1] if self.points else 0.0
+        # ---------------------
+        # 4) Predicted Time to Level
+        # ---------------------
         secs_left = self.compute_time_to_100()
         time_str = format_duration(secs_left)
 
-        with Progress(
+        time_bar = Progress(
             TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
             BarColumn(bar_width=30, complete_style="magenta"),
             TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
             TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
-            console=console,
             transient=False,
-        ) as progress:
-            progress.add_task("", total=100, completed=current_exp)
-            progress.refresh()
+            auto_refresh=False
+        )
+        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():
     """
-    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)
-    window = BorealisOverlay()
+    window = BorealisOverlay()   # We'll inject Live momentarily
+
     window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
     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__":
     main()