Implemented Comparison Nodes and Low Health Alerts
This commit is contained in:
parent
9c2e287b72
commit
39ede691d4
Binary file not shown.
BIN
Nodes/__pycache__/comparison_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/comparison_node.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,12 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 Qt import QtWidgets, QtGui
|
||||||
from OdenGraphQt import BaseNode
|
from OdenGraphQt import BaseNode
|
||||||
|
128
Nodes/comparison_node.py
Normal file
128
Nodes/comparison_node.py
Normal 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()
|
@ -36,7 +36,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127):
|
|||||||
|
|
||||||
class CharacterStatusNode(BaseNode):
|
class CharacterStatusNode(BaseNode):
|
||||||
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
|
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
|
||||||
NODE_NAME = 'Character Status'
|
NODE_NAME = 'Flyff - Character Status'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(CharacterStatusNode, self).__init__()
|
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("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.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
|
# Start polling timer
|
||||||
self.timer = QtCore.QTimer()
|
self.timer = QtCore.QTimer()
|
||||||
@ -95,7 +95,7 @@ class CharacterStatusNode(BaseNode):
|
|||||||
else:
|
else:
|
||||||
print(f"[WARNING] Unexpected API key: {key} (not mapped)")
|
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.update()
|
||||||
self.transmit_data()
|
self.transmit_data()
|
||||||
|
|
||||||
@ -104,14 +104,14 @@ class CharacterStatusNode(BaseNode):
|
|||||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
||||||
else:
|
else:
|
||||||
print("[ERROR] Unexpected API response format (not a dict):", data)
|
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:
|
else:
|
||||||
print(f"[ERROR] API request failed with status code {response.status_code}")
|
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:
|
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] Error polling API in CharacterStatusNode:", str(e))
|
||||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
||||||
|
|
||||||
|
22
Nodes/flyff_low_health_alert_node.py
Normal file
22
Nodes/flyff_low_health_alert_node.py
Normal 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
104
borealis_transparent.py
Normal 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
80
screen_overlays.py
Normal 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_())
|
Loading…
x
Reference in New Issue
Block a user