Files
chaos-protocol/game/rules.py

566 lines
19 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 + 10
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) * 10
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 + 7
var_index += 1
def _build_block(self, b: Block, x: int, y: int) -> int:
is_wrap = b.rule_type != "do"
h, w = 42, 280
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, 280, 44).intersection(arcade.LBWH(self.dragged_rule_ui.x, self.dragged_rule_ui.y - 44, 280, 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, 280, 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_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
if self.trash_sprite.rect.intersection(arcade.LBWH(block_vec.x, block_vec.y, 280, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1:
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_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
if self.trash_sprite.rect.intersection(arcade.LBWH(block_vec.x, block_vec.y, 280, 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()