mirror of
https://github.com/csd4ni3l/chaos-protocol.git
synced 2026-01-01 04:23:43 +01:00
579 lines
20 KiB
Python
579 lines
20 KiB
Python
from utils.constants import (
|
|
DO_RULES,
|
|
IF_RULES,
|
|
TRIGGER_RULES,
|
|
FOR_RULES,
|
|
NEEDS_SHAPE,
|
|
PROVIDES_SHAPE,
|
|
button_style,
|
|
slider_style,
|
|
dropdown_style,
|
|
DO_COLOR,
|
|
IF_COLOR,
|
|
FOR_COLOR,
|
|
TRIGGER_COLOR,
|
|
RULE_DEFAULTS,
|
|
VAR_TYPES
|
|
)
|
|
from typing import List
|
|
from utils.preload import button_texture, button_hovered_texture, trash_bin
|
|
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
|
|
from dataclasses import dataclass, field
|
|
import arcade, arcade.gui, pyglet, random, re
|
|
|
|
def get_rule_dict(rule_type):
|
|
if rule_type == "if":
|
|
return IF_RULES
|
|
elif rule_type == "for":
|
|
return FOR_RULES
|
|
elif rule_type == "trigger":
|
|
return TRIGGER_RULES
|
|
elif rule_type == "do":
|
|
return DO_RULES
|
|
|
|
@dataclass
|
|
class VarBlock:
|
|
x: float
|
|
y: float
|
|
label: str
|
|
var_type: str
|
|
connected_rule_num: str
|
|
value: str | int
|
|
|
|
@dataclass
|
|
class Block:
|
|
x: float
|
|
y: float
|
|
label: str
|
|
rule_type: str
|
|
rule: str
|
|
rule_num: int
|
|
vars: List["VarBlock"] = field(default_factory=list)
|
|
children: List["Block"] = field(default_factory=list)
|
|
|
|
class BlockRenderer:
|
|
def __init__(self, blocks: List[Block], indent: int = 12):
|
|
self.blocks = blocks
|
|
self.indent = indent
|
|
self.shapes = pyglet.graphics.Batch()
|
|
self.shapes_by_rule_num = {}
|
|
self.text_objects = []
|
|
self.text_by_rule_num = {}
|
|
self.var_widgets = {}
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
for shapes_list in self.shapes_by_rule_num.values():
|
|
for shape in shapes_list:
|
|
shape.delete()
|
|
|
|
for text_list in self.text_by_rule_num.values():
|
|
for text in text_list:
|
|
text.delete()
|
|
|
|
self.shapes = pyglet.graphics.Batch()
|
|
self.shapes_by_rule_num = {}
|
|
self.text_objects = []
|
|
self.text_by_rule_num = {}
|
|
self.var_widgets = {}
|
|
for b in self.blocks.values():
|
|
self._build_block(b, b.x, b.y)
|
|
|
|
def _build_var_ui(self, var: VarBlock, x: int, y: int, rule_num: int) -> tuple:
|
|
var_width = max(60, len(str(var.value)) * 8 + 20)
|
|
var_height = 24
|
|
var_color = (255, 255, 255)
|
|
var_rect = pyglet.shapes.BorderedRectangle(
|
|
x, y - var_height // 2, var_width, var_height,
|
|
2, var_color, arcade.color.BLACK, batch=self.shapes
|
|
)
|
|
|
|
var_text = pyglet.text.Label(
|
|
text=str(var.value),
|
|
x=x + var_width // 2,
|
|
y=y,
|
|
color=arcade.color.BLACK,
|
|
font_size=10,
|
|
anchor_x='center',
|
|
anchor_y='center'
|
|
)
|
|
|
|
if rule_num not in self.shapes_by_rule_num:
|
|
self.shapes_by_rule_num[rule_num] = []
|
|
if rule_num not in self.text_by_rule_num:
|
|
self.text_by_rule_num[rule_num] = []
|
|
if rule_num not in self.var_widgets:
|
|
self.var_widgets[rule_num] = []
|
|
|
|
self.shapes_by_rule_num[rule_num].append(var_rect)
|
|
self.text_by_rule_num[rule_num].append(var_text)
|
|
self.text_objects.append(var_text)
|
|
|
|
self.var_widgets[rule_num].append({
|
|
'var': var,
|
|
'rect': var_rect,
|
|
'text': var_text,
|
|
'x': x,
|
|
'y': y,
|
|
'width': var_width,
|
|
'height': var_height
|
|
})
|
|
|
|
return var_width, var_height
|
|
|
|
def _build_block_with_vars(self, b: Block, x: int, y: int) -> None:
|
|
lx, ly = x, y - 42
|
|
|
|
current_x = lx + 14
|
|
current_y = ly + 28
|
|
|
|
pattern = r'(?:^| )([a-z])(?= |$)'
|
|
parts = re.split(pattern, b.label)
|
|
|
|
var_index = 0
|
|
for i, part in enumerate(parts):
|
|
if i % 2 == 0:
|
|
if part:
|
|
text_obj = pyglet.text.Label(
|
|
text=part,
|
|
x=current_x,
|
|
y=current_y - 3,
|
|
color=arcade.color.BLACK,
|
|
font_size=12,
|
|
weight="bold"
|
|
)
|
|
self.text_objects.append(text_obj)
|
|
self.text_by_rule_num[b.rule_num].append(text_obj)
|
|
|
|
current_x += len(part) * 12
|
|
else:
|
|
if var_index < len(b.vars):
|
|
var = b.vars[var_index]
|
|
var_width, var_height = self._build_var_ui(
|
|
var, current_x, current_y, b.rule_num
|
|
)
|
|
current_x += var_width + 10
|
|
var_index += 1
|
|
|
|
def _build_block(self, b: Block, x: int, y: int) -> int:
|
|
is_wrap = b.rule_type != "do"
|
|
h, w = 42, 380
|
|
|
|
if b.rule_type == "if":
|
|
color = IF_COLOR
|
|
elif b.rule_type == "trigger":
|
|
color = TRIGGER_COLOR
|
|
elif b.rule_type == "do":
|
|
color = DO_COLOR
|
|
elif b.rule_type == "for":
|
|
color = FOR_COLOR
|
|
|
|
lx, ly = x, y - h
|
|
|
|
if b.rule_num not in self.shapes_by_rule_num:
|
|
self.shapes_by_rule_num[b.rule_num] = []
|
|
if b.rule_num not in self.text_by_rule_num:
|
|
self.text_by_rule_num[b.rule_num] = []
|
|
|
|
rect = pyglet.shapes.BorderedRectangle(lx, ly, w, h, 2, color, arcade.color.BLACK, batch=self.shapes)
|
|
self.shapes_by_rule_num[b.rule_num].append(rect)
|
|
|
|
if b.vars:
|
|
self._build_block_with_vars(b, x, y)
|
|
else:
|
|
text_obj = pyglet.text.Label(
|
|
text=b.label,
|
|
x=lx + 7,
|
|
y=ly + 20,
|
|
color=arcade.color.BLACK,
|
|
font_size=12,
|
|
weight="bold"
|
|
)
|
|
self.text_objects.append(text_obj)
|
|
self.text_by_rule_num[b.rule_num].append(text_obj)
|
|
|
|
next_y = ly
|
|
if is_wrap:
|
|
iy = next_y
|
|
for child in b.children:
|
|
child.x = lx + self.indent + 5
|
|
child.y = iy
|
|
iy = self._build_block(child, lx + self.indent + 5, iy)
|
|
|
|
bar_h = next_y - iy
|
|
bar_filled = pyglet.shapes.Rectangle(lx + 2, iy + 2, self.indent, bar_h, color, batch=self.shapes)
|
|
line1 = pyglet.shapes.Line(lx, next_y, lx, iy, 2, arcade.color.BLACK, batch=self.shapes)
|
|
bottom = pyglet.shapes.BorderedRectangle(lx, iy - 8, w, 24, 2, color, arcade.color.BLACK, batch=self.shapes)
|
|
|
|
self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom])
|
|
|
|
return iy - 24
|
|
else:
|
|
for child in b.children:
|
|
child.x = lx
|
|
child.y = next_y
|
|
ly = self._build_block(child, lx, next_y)
|
|
return ly - 16
|
|
|
|
def move_block(self, x, y, rule_num):
|
|
for element in self.shapes_by_rule_num[rule_num] + self.text_by_rule_num[rule_num]:
|
|
element.x += x
|
|
element.y += y
|
|
|
|
if rule_num in self.var_widgets:
|
|
for widget in self.var_widgets[rule_num]:
|
|
widget['x'] += x
|
|
widget['y'] += y
|
|
|
|
block = self._find_block(rule_num)
|
|
|
|
for child in block.children:
|
|
self.move_block(x, y, child.rule_num)
|
|
|
|
def get_var_at_position(self, x, y):
|
|
for rule_num, widgets in self.var_widgets.items():
|
|
for widget in widgets:
|
|
wx, wy = widget['x'], widget['y']
|
|
ww, wh = widget['width'], widget['height']
|
|
if (wx <= x <= wx + ww and
|
|
wy - wh // 2 <= y <= wy + wh // 2):
|
|
return widget['var'], rule_num
|
|
return None, None
|
|
|
|
def _find_block(self, rule_num):
|
|
if rule_num in self.blocks:
|
|
return self.blocks[rule_num]
|
|
|
|
for block in self.blocks.values():
|
|
found = self._find_block_recursive(block, rule_num)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
def _find_block_recursive(self, block, rule_num):
|
|
for child in block.children:
|
|
if child.rule_num == rule_num:
|
|
return child
|
|
found = self._find_block_recursive(child, rule_num)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
def draw(self):
|
|
self.shapes.draw()
|
|
for t in self.text_objects:
|
|
t.draw()
|
|
|
|
class VarEditDialog(arcade.gui.UIAnchorLayout):
|
|
def __init__(self, var: VarBlock, on_save, on_cancel):
|
|
super().__init__()
|
|
self.var = var
|
|
self.on_save_callback = on_save
|
|
self.on_cancel_callback = on_cancel
|
|
|
|
self.background = self.add(
|
|
arcade.gui.UISpace(color=(0, 0, 0, 180)),
|
|
anchor_x="center",
|
|
anchor_y="center"
|
|
)
|
|
|
|
dialog_box = arcade.gui.UIBoxLayout(
|
|
space_between=10,
|
|
width=300,
|
|
height=200
|
|
)
|
|
|
|
dialog_box.with_padding(all=20)
|
|
dialog_box.with_background(color=(60, 60, 80))
|
|
|
|
dialog_box.add(arcade.gui.UILabel(
|
|
text=f"Edit {var.label}",
|
|
font_size=16,
|
|
text_color=arcade.color.WHITE
|
|
))
|
|
|
|
if var.var_type == "variable":
|
|
self.input_field = arcade.gui.UIInputText(
|
|
text=str(var.value),
|
|
width=260,
|
|
height=40
|
|
)
|
|
dialog_box.add(self.input_field)
|
|
elif var.var_type in ["shape_type", "target_type", "color", "key_input", "comparison"]:
|
|
from utils.constants import VAR_OPTIONS
|
|
options = VAR_OPTIONS[var.var_type]
|
|
self.dropdown = arcade.gui.UIDropdown(
|
|
default=str(var.value),
|
|
options=options,
|
|
width=260,
|
|
height=40,
|
|
style=dropdown_style
|
|
)
|
|
dialog_box.add(self.dropdown)
|
|
elif var.var_type == "size":
|
|
self.slider = arcade.gui.UISlider(
|
|
value=int(var.value),
|
|
min_value=1,
|
|
max_value=200,
|
|
width=260,
|
|
height=40,
|
|
style=slider_style
|
|
)
|
|
dialog_box.add(self.slider)
|
|
|
|
button_layout = arcade.gui.UIBoxLayout(vertical=False, space_between=10)
|
|
|
|
save_btn = arcade.gui.UIFlatButton(
|
|
text="Save",
|
|
width=125,
|
|
height=40
|
|
)
|
|
save_btn.on_click = self._on_save
|
|
|
|
cancel_btn = arcade.gui.UIFlatButton(
|
|
text="Cancel",
|
|
width=125,
|
|
height=40
|
|
)
|
|
cancel_btn.on_click = self._on_cancel
|
|
|
|
button_layout.add(save_btn)
|
|
button_layout.add(cancel_btn)
|
|
dialog_box.add(button_layout)
|
|
|
|
self.add(dialog_box, anchor_x="center", anchor_y="center")
|
|
|
|
def _on_save(self, event):
|
|
if hasattr(self, 'input_field'):
|
|
try:
|
|
self.var.value = int(self.input_field.text)
|
|
except ValueError:
|
|
self.var.value = self.input_field.text
|
|
elif hasattr(self, 'dropdown'):
|
|
self.var.value = self.dropdown.value
|
|
elif hasattr(self, 'slider'):
|
|
self.var.value = int(self.slider.value)
|
|
|
|
self.on_save_callback()
|
|
|
|
def _on_cancel(self, event):
|
|
self.on_cancel_callback()
|
|
|
|
class RuleUI(arcade.gui.UIAnchorLayout):
|
|
def __init__(self, window: arcade.Window):
|
|
super().__init__(size_hint=(1, 0.875))
|
|
|
|
self.window = window
|
|
self.current_rule_num = 0
|
|
self.rule_values = {}
|
|
self.var_edit_dialog = None
|
|
|
|
self.rulesets: dict[int, Block] = {}
|
|
|
|
self.block_renderer = BlockRenderer(self.rulesets)
|
|
self.camera = arcade.Camera2D()
|
|
|
|
self.dragged_rule_ui: Block | None = None
|
|
|
|
self.rules_label = self.add(
|
|
arcade.gui.UILabel(
|
|
text="Rules", font_size=20, text_color=arcade.color.WHITE
|
|
),
|
|
anchor_x="center",
|
|
anchor_y="top"
|
|
)
|
|
|
|
self.add(
|
|
arcade.gui.UISpace(
|
|
height=self.window.height / 70, width=self.window.width * 0.25
|
|
)
|
|
)
|
|
|
|
self.create_sidebar = self.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 1), vertical=False, space_between=5), anchor_x="left", anchor_y="bottom")
|
|
|
|
self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen
|
|
self.scroll_area.scroll_speed = 0
|
|
self.create_sidebar.add(self.scroll_area)
|
|
|
|
self.scrollbar = UIScrollBar(self.scroll_area)
|
|
self.scrollbar.size_hint = (0.075, 1)
|
|
self.create_sidebar.add(self.scrollbar)
|
|
|
|
self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10))
|
|
|
|
self.add_rule_create_box("trigger")
|
|
self.add_rule_create_box("if")
|
|
self.add_rule_create_box("do")
|
|
self.add_rule_create_box("for")
|
|
|
|
self.trash_spritelist = arcade.SpriteList()
|
|
self.trash_sprite = trash_bin
|
|
self.trash_sprite.scale = 0.5
|
|
self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2)
|
|
self.trash_spritelist.append(self.trash_sprite)
|
|
|
|
def add_rule_create_box(self, rule_type):
|
|
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
|
|
self.create_box.add(arcade.gui.UILabel(text=f"{rule_type.capitalize()} Rules", font_size=18))
|
|
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
|
|
for rule in get_rule_dict(rule_type):
|
|
create_button = self.create_box.add(arcade.gui.UITextureButton(text=RULE_DEFAULTS[rule_type][rule][0], width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
|
|
create_button.on_click = lambda event, rule=rule: self.add_rule(rule_type, rule)
|
|
|
|
def generate_pos(self):
|
|
return random.randint(
|
|
self.window.width * 0.1, int(self.window.width * 0.9)
|
|
), random.randint(self.window.height * 0.1, int(self.window.height * 0.7))
|
|
|
|
def add_rule(self, rule_type, rule):
|
|
rule_dict = get_rule_dict(rule_type)[rule]
|
|
rule_box = Block(
|
|
*self.generate_pos(),
|
|
RULE_DEFAULTS[rule_type][rule][0],
|
|
rule_type,
|
|
rule,
|
|
self.current_rule_num,
|
|
[
|
|
VarBlock(
|
|
*self.generate_pos(),
|
|
VAR_TYPES[var_type],
|
|
var_type,
|
|
self.current_rule_num,
|
|
RULE_DEFAULTS[rule_type][rule][1][n]
|
|
)
|
|
|
|
for n, var_type in enumerate(rule_dict["user_vars"])
|
|
],
|
|
[]
|
|
)
|
|
|
|
self.rulesets[self.current_rule_num] = rule_box
|
|
self.current_rule_num += 1
|
|
self.block_renderer.refresh()
|
|
|
|
return rule_box
|
|
|
|
def draw(self):
|
|
self.block_renderer.draw()
|
|
|
|
def draw_unproject(self):
|
|
self.trash_spritelist.draw()
|
|
|
|
def drag_n_drop_check(self, blocks):
|
|
if self.dragged_rule_ui.rule_type == "trigger":
|
|
return
|
|
|
|
for block in blocks:
|
|
if block == self.dragged_rule_ui or (self.dragged_rule_ui.rule in NEEDS_SHAPE and block.rule not in PROVIDES_SHAPE):
|
|
continue
|
|
|
|
if arcade.LBWH(block.x, block.y - 44, 380, 44).intersection(arcade.LBWH(self.dragged_rule_ui.x, self.dragged_rule_ui.y - 44, 380, 44)):
|
|
block.children.append(self.dragged_rule_ui)
|
|
del self.rulesets[self.dragged_rule_ui.rule_num]
|
|
self.block_renderer.refresh()
|
|
break
|
|
else:
|
|
self.drag_n_drop_check(block.children)
|
|
|
|
def remove_from_parent(self, block_to_remove, parents):
|
|
for parent in parents:
|
|
if block_to_remove in parent.children:
|
|
self.rulesets[block_to_remove.rule_num] = block_to_remove
|
|
parent.children.remove(block_to_remove)
|
|
return True
|
|
if self.remove_from_parent(block_to_remove, parent.children):
|
|
return True
|
|
return False
|
|
|
|
def press_check(self, event, blocks):
|
|
for block in blocks:
|
|
if block == self.dragged_rule_ui:
|
|
continue
|
|
|
|
projected_vec = self.camera.unproject((event.x, event.y))
|
|
if arcade.LBWH(block.x, block.y - 44, 380, 44).point_in_rect((projected_vec.x, projected_vec.y)):
|
|
if block not in list(self.rulesets.values()): # its children
|
|
self.remove_from_parent(block, list(self.rulesets.values()))
|
|
self.block_renderer.refresh()
|
|
self.dragged_rule_ui = block
|
|
break
|
|
else:
|
|
self.press_check(event, block.children)
|
|
|
|
def on_event(self, event):
|
|
if self.var_edit_dialog:
|
|
super().on_event(event)
|
|
return
|
|
|
|
super().on_event(event)
|
|
|
|
if isinstance(event, arcade.gui.UIMouseDragEvent):
|
|
if event.buttons == arcade.MOUSE_BUTTON_LEFT:
|
|
if self.dragged_rule_ui is not None:
|
|
self.dragged_rule_ui.x += event.dx
|
|
self.dragged_rule_ui.y += event.dy
|
|
self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num)
|
|
|
|
elif isinstance(event, arcade.gui.UIMousePressEvent):
|
|
projected_vec = self.camera.unproject((event.x, event.y))
|
|
var, _ = self.block_renderer.get_var_at_position(projected_vec.x, projected_vec.y)
|
|
|
|
if var:
|
|
self.open_var_edit_dialog(var)
|
|
return
|
|
|
|
self.press_check(event, list(self.rulesets.values()))
|
|
|
|
elif isinstance(event, arcade.gui.UIMouseReleaseEvent):
|
|
if self.dragged_rule_ui:
|
|
block_screen_pos = self.camera.project((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
|
|
|
|
block_rect = arcade.LBWH(block_screen_pos[0], block_screen_pos[1], 380, 44)
|
|
trash_rect = arcade.LBWH(
|
|
self.trash_sprite.center_x - self.trash_sprite.width / 2,
|
|
self.trash_sprite.center_y - self.trash_sprite.height / 2,
|
|
self.trash_sprite.width,
|
|
self.trash_sprite.height
|
|
)
|
|
|
|
if block_rect.intersection(trash_rect):
|
|
self.remove_from_parent(self.dragged_rule_ui, list(self.rulesets.values()))
|
|
|
|
if self.dragged_rule_ui.rule_num in self.rulesets:
|
|
del self.rulesets[self.dragged_rule_ui.rule_num]
|
|
|
|
self.dragged_rule_ui = None
|
|
self.block_renderer.refresh()
|
|
return
|
|
|
|
self.drag_n_drop_check(list(self.rulesets.values()))
|
|
|
|
self.dragged_rule_ui = None
|
|
|
|
def open_var_edit_dialog(self, var: VarBlock):
|
|
def on_save():
|
|
self.close_var_edit_dialog()
|
|
self.block_renderer.refresh()
|
|
|
|
def on_cancel():
|
|
self.close_var_edit_dialog()
|
|
|
|
self.var_edit_dialog = VarEditDialog(var, on_save, on_cancel)
|
|
self.add(self.var_edit_dialog)
|
|
|
|
def close_var_edit_dialog(self):
|
|
if self.var_edit_dialog:
|
|
self.remove(self.var_edit_dialog)
|
|
self.var_edit_dialog = None
|
|
self.trigger_full_render()
|
|
|
|
def on_update(self, dt):
|
|
if self.dragged_rule_ui:
|
|
block_screen_pos = self.camera.project((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
|
|
if self.trash_sprite.rect.intersection(arcade.LBWH(block_screen_pos[0], block_screen_pos[1], 380, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1:
|
|
self.trash_sprite.update_animation()
|
|
else:
|
|
self.trash_sprite.time = 0
|
|
self.trash_sprite.update_animation()
|
|
|