Implemented Comparison Nodes and Low Health Alerts

This commit is contained in:
Nicole Rappe 2025-02-13 00:31:06 -07:00
parent 9c2e287b72
commit 39ede691d4
12 changed files with 340 additions and 14 deletions

Binary file not shown.

View File

@ -1,12 +1,4 @@
#!/usr/bin/env python3
"""
Enhanced Backdrop Node (Inherited from BaseNode)
Features:
- Inherits from `BaseNode` so it can be discovered in your node scanning.
- Custom context menu to rename (set title) or pick a new color.
- Forces geometry updates to reduce "ghosting" or partial redraws.
"""
from Qt import QtWidgets, QtGui
from OdenGraphQt import BaseNode

128
Nodes/comparison_node.py Normal file
View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
Comparison Node:
- Inputs: Two input ports ("A" and "B").
- Output: One output port ("Result").
- Operation: A dropdown (combo menu) to select:
Equal (==), Not Equal (!=), Greater Than (>), Less Than (<),
Greater Than or Equal (>=), Less Than or Equal (<=).
- Displays the computed result in a read-only text box labeled "Result".
"""
from OdenGraphQt import BaseNode
class ComparisonNode(BaseNode):
__identifier__ = 'bunny-lab.io.comparison_node'
NODE_NAME = 'Comparison Node'
def __init__(self):
super(ComparisonNode, self).__init__()
# ----------------------------------------------------------------------
# Initialization Section:
# - Create two input ports: A, B
# - Create one output port: Result
# - Add a combo box for logical operator selection
# - Add a text input for displaying the computed result
# ----------------------------------------------------------------------
self.add_input('A')
self.add_input('B')
self.add_output('Result')
# Operator combo box (==, !=, >, <, >=, <=)
self.add_combo_menu('operator', 'Operator', items=[
'Equal (==)',
'Not Equal (!=)',
'Greater Than (>)',
'Less Than (<)',
'Greater Than or Equal (>=)',
'Less Than or Equal (<=)'
])
# Text input for displaying the computed result.
# We'll make it read-only by accessing the underlying QLineEdit.
self.add_text_input('calc_result', 'Result', text='0')
result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper
if result_widget:
# Get the underlying QLineEdit
line_edit = result_widget.get_custom_widget()
# Make the QLineEdit read-only
line_edit.setReadOnly(True)
self.value = 0
self.set_name("Comparison Node")
self.process_input()
def process_input(self, event=None):
"""
Compute Section:
- For each input port (A, B), if connected, grab the 'value' from
the upstream node; otherwise default to 0.0.
- Convert to float when possible, apply the selected comparison operator,
update the "Result" text box, node title, and output port.
"""
# Gather input A
input_a = self.input(0)
if input_a and input_a.connected_ports():
a_raw = input_a.connected_ports()[0].node().get_property('value')
else:
a_raw = 0.0
# Gather input B
input_b = self.input(1)
if input_b and input_b.connected_ports():
b_raw = input_b.connected_ports()[0].node().get_property('value')
else:
b_raw = 0.0
# Convert raw inputs to float if possible, otherwise keep as-is for string comparison.
try:
a_val = float(a_raw)
b_val = float(b_raw)
except (ValueError, TypeError):
a_val = a_raw
b_val = b_raw
# Retrieve the selected operator from the combo box.
operator = self.get_property('operator')
result = False
if operator == 'Equal (==)':
result = a_val == b_val
elif operator == 'Not Equal (!=)':
result = a_val != b_val
elif operator == 'Greater Than (>)':
result = a_val > b_val
elif operator == 'Less Than (<)':
result = a_val < b_val
elif operator == 'Greater Than or Equal (>=)':
result = a_val >= b_val
elif operator == 'Less Than or Equal (<=)':
result = a_val <= b_val
# Convert boolean result to integer (1 for True, 0 for False)
self.value = 1 if result else 0
# Update the read-only text input and node title.
self.set_property('calc_result', str(self.value))
# Transmit the numeric result to any connected output nodes.
output_port = self.output(0)
if output_port and output_port.connected_ports():
for cp in output_port.connected_ports():
connected_node = cp.node()
if hasattr(connected_node, 'receive_data'):
connected_node.receive_data(self.value, source_port_name='Result')
def on_input_connected(self, input_port, output_port):
self.process_input()
def on_input_disconnected(self, input_port, output_port):
self.process_input()
def property_changed(self, property_name):
if property_name in ['operator']:
self.process_input()
def receive_data(self, data, source_port_name=None):
self.process_input()

View File

@ -36,7 +36,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127):
class CharacterStatusNode(BaseNode):
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
NODE_NAME = 'Character Status'
NODE_NAME = 'Flyff - Character Status'
def __init__(self):
super(CharacterStatusNode, self).__init__()
@ -58,7 +58,7 @@ class CharacterStatusNode(BaseNode):
self.add_output("FP: Total", painter_func=get_draw_stat_port((36, 197, 28)))
self.add_output("EXP", painter_func=get_draw_stat_port((52, 195, 250)))
self.set_name("Character Status (API Disconnected)")
self.set_name("Flyff - Character Status (API Disconnected)")
# Start polling timer
self.timer = QtCore.QTimer()
@ -95,7 +95,7 @@ class CharacterStatusNode(BaseNode):
else:
print(f"[WARNING] Unexpected API key: {key} (not mapped)")
self.set_name("Character Status (API Connected)")
self.set_name("Flyff - Character Status (API Connected)")
self.update()
self.transmit_data()
@ -104,14 +104,14 @@ class CharacterStatusNode(BaseNode):
print("[ERROR] Stack Trace:\n", traceback.format_exc())
else:
print("[ERROR] Unexpected API response format (not a dict):", data)
self.set_name("Character Status (API Disconnected)")
self.set_name("Flyff - Character Status (API Disconnected)")
else:
print(f"[ERROR] API request failed with status code {response.status_code}")
self.set_name("Character Status (API Disconnected)")
self.set_name("Flyff - Character Status (API Disconnected)")
except Exception as e:
self.set_name("Character Status (API Disconnected)")
self.set_name("Flyff - Character Status (API Disconnected)")
print("[ERROR] Error polling API in CharacterStatusNode:", str(e))
print("[ERROR] Stack Trace:\n", traceback.format_exc())

View File

@ -0,0 +1,22 @@
from OdenGraphQt import BaseNode
from OdenGraphQt.constants import NodePropWidgetEnum
from OdenGraphQt.widgets.node_widgets import NodeLineEditValidatorCheckBox
class CheckboxNode(BaseNode):
# set a unique node identifier.
__identifier__ = 'bunny-lab.io.flyff_low_health_alert_node'
# set the initial default node name.
NODE_NAME = 'Flyff - Low Health Alert'
def __init__(self):
super(CheckboxNode, self).__init__()
# Create checkboxes to decide which kind of alert(s) to utilize.
self.add_checkbox('cb_1', '', 'Sound Alert', True)
self.add_checkbox('cb_2', '', 'Visual Alert', True)
# Create Input Port
self.add_input('Toggle (1 = On | 0 = Off)', color=(200, 100, 0))
#self.add_output('out', color=(0, 100, 200))

104
borealis_transparent.py Normal file
View File

@ -0,0 +1,104 @@
import os
import sys
import pkgutil
import importlib
import inspect
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QMenu, QUndoStack
from PyQt5.QtCore import Qt, QTimer, QRect
from PyQt5.QtGui import QColor, QPainter, QPen, QBrush
from OdenGraphQt import NodeGraph, BaseNode
# Force qtpy to use PyQt5 explicitly
os.environ["QT_API"] = "pyqt5"
# --- PATCH OdenGraphQt to use QtWidgets.QUndoStack instead of QtGui.QUndoStack ---
import qtpy.QtGui
import qtpy.QtWidgets
if not hasattr(qtpy.QtGui, "QUndoStack"): # Ensure patching only if necessary
qtpy.QtGui.QUndoStack = qtpy.QtWidgets.QUndoStack
# --- END PATCH ---
class TransparentGraphWindow(QMainWindow):
def __init__(self):
super().__init__()
# Enable transparency & always-on-top behavior
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# Get full-screen size for overlay
screen_geometry = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geometry)
# Create Node Graph
self.graph = NodeGraph()
self.graph.widget.setParent(self)
self.graph.widget.setGeometry(self.rect())
# Make bottom-left corner interactive for context menu
self.context_menu_area = QRect(10, self.height() - 40, 50, 30)
# Load nodes dynamically
self.import_nodes()
# Global update timer for processing nodes
self.timer = QTimer()
self.timer.timeout.connect(self.update_nodes)
self.timer.start(500)
def import_nodes(self):
"""Dynamically import all custom node classes from the 'Nodes' package."""
try:
package_name = 'Nodes'
package = importlib.import_module(package_name)
for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
self.graph.register_node(obj)
except Exception as e:
print(f"Error loading nodes: {e}")
def update_nodes(self):
"""Calls process_input() on all nodes, if applicable."""
for node in self.graph.all_nodes():
if hasattr(node, "process_input"):
try:
node.process_input()
except Exception as e:
print(f"Error processing node {node}: {e}")
def mousePressEvent(self, event):
"""Override mouse press to handle context menu in bottom-left area."""
if event.button() == Qt.RightButton and self.context_menu_area.contains(event.pos()):
self.show_context_menu(event.globalPos())
def show_context_menu(self, position):
"""Displays a right-click context menu."""
menu = QMenu(self)
quit_action = QAction("Quit", self)
quit_action.triggered.connect(self.close)
menu.addAction(quit_action)
menu.exec_(position)
def paintEvent(self, event):
"""Render transparent overlay and the small clickable menu area."""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Draw semi-transparent context menu area
painter.setBrush(QBrush(QColor(50, 50, 50, 150))) # Dark semi-transparent box
painter.setPen(QPen(QColor(200, 200, 200, 200)))
painter.drawRect(self.context_menu_area)
painter.setFont(self.font())
painter.setPen(QColor(255, 255, 255))
painter.drawText(self.context_menu_area, Qt.AlignCenter, "Menu")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TransparentGraphWindow()
window.show()
sys.exit(app.exec_())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

80
screen_overlays.py Normal file
View File

@ -0,0 +1,80 @@
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint
from PyQt5.QtGui import QPainter, QPen, QColor, QFont, QFontMetrics
class OverlayCanvas(QWidget):
"""
UI overlay for drawing and interacting with on-screen elements.
"""
def __init__(self, parent=None):
super().__init__(parent)
# **Full-screen overlay**
screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo) # Set to full screen
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_NoSystemBackground, True)
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
self.setAttribute(Qt.WA_AlwaysStackOnTop, True)
# **Helper Object: Low Health Alert**
self.helper_LowHealthAlert = QRect(250, 300, 900, 35) # Adjusted width and height
self.dragging = False
self.resizing = False
self.drag_offset = QPoint()
def paintEvent(self, event):
"""Draw the helper overlay objects."""
painter = QPainter(self)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(255, 0, 0)) # Solid red rectangle
painter.drawRect(self.helper_LowHealthAlert)
# Draw bold white text centered within the rectangle
font = QFont("Arial", 14, QFont.Bold) # Scaled text
painter.setFont(font)
painter.setPen(QColor(255, 255, 255))
text = "LOW HEALTH"
metrics = QFontMetrics(font)
text_width = metrics.horizontalAdvance(text)
text_height = metrics.height()
text_x = self.helper_LowHealthAlert.center().x() - text_width // 2
text_y = self.helper_LowHealthAlert.center().y() + text_height // 4
painter.drawText(text_x, text_y, text)
def mousePressEvent(self, event):
"""Detect clicks for dragging and resizing the helper object."""
if event.button() == Qt.LeftButton:
if self.helper_LowHealthAlert.contains(event.pos()):
if event.pos().x() > self.helper_LowHealthAlert.right() - 10 and event.pos().y() > self.helper_LowHealthAlert.bottom() - 10:
self.resizing = True
else:
self.dragging = True
self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft()
def mouseMoveEvent(self, event):
"""Handle dragging and resizing movements."""
if self.dragging:
self.helper_LowHealthAlert.moveTopLeft(event.pos() - self.drag_offset)
self.update()
elif self.resizing:
new_width = max(150, event.pos().x() - self.helper_LowHealthAlert.x())
new_height = max(20, event.pos().y() - self.helper_LowHealthAlert.y())
self.helper_LowHealthAlert.setSize(new_width, new_height)
self.update()
def mouseReleaseEvent(self, event):
"""End dragging or resizing event."""
self.dragging = False
self.resizing = False
if __name__ == '__main__':
app_gui = QApplication(sys.argv)
overlay_window = OverlayCanvas()
overlay_window.show()
sys.exit(app_gui.exec_())