Fix file manager sprite add submitting with / at the end, fix change_size not working for textured, make texturedrectangles actually work, fix importing breaking rule system and refresh sprites after import, add separate import and export file managers, reset submitted content of file managers after submit, fix TexturedRectangles staying at 0, 0, fix some performance issues and update README

This commit is contained in:
csd4ni3l
2025-12-07 23:45:20 +01:00
parent 3a7e40d833
commit c25ffe1a62
4 changed files with 85 additions and 43 deletions

View File

@@ -1,6 +1,10 @@
Chaos Protocol is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules.
**Chaos Protocol** is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules.
Basically a framework of sorts, which can even be random!
The project is a huge WIP! You can't do much yet, but you have basic rules and simulation.
The game has Scratch-like blocks and conditions, and everything you probably need for a simple game.
The problem: it hasn't been well tested.
## Speed is a big problem of the project, especially collision detections, which are very expensive.
I am not using spatial hashing right now, and i had no time to optimize the code. I'm sorry.
[![Demo Video](https://img.youtube.com/vi/iMB4mmjTIB4/hqdefault.jpg)](https://youtu.be/iMB4mmjTIB4)

View File

@@ -55,8 +55,8 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.submit_button.visible = self.mode == "export"
def submit(self, content):
self.submitted_content = content if self.mode == "import" else f"{content}/{self.filename_input.text}"
self.submitted_content = content if self.mode == "import" else os.path.join(content, self.filename_input.text)
def get_content(self, directory):
if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30:
try:
@@ -113,11 +113,11 @@ class FileManager(arcade.gui.UIAnchorLayout):
for file in self.get_content(self.current_directory):
self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=file, style=button_style, width=self.filemanager_width / 1.5, height=self.filemanager_height * 0.05,)))
if os.path.isdir(f"{self.current_directory}/{file}"):
self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(directory)
if os.path.isdir(os.path.join(self.current_directory, file)):
self.file_buttons[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory)
else:
self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file)
self.file_buttons[-1].on_click = lambda event, file=os.path.join(self.current_directory, file): self.submit(file)
def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths
directory = directory[1:]

View File

@@ -6,7 +6,7 @@ from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_textur
from utils.constants import button_style, DO_RULES, IF_RULES, SPRITES, ALLOWED_INPUT
from game.rules import RuleUI, Block, VarBlock
from game.sprites import BaseShape, Rectangle, Circle, Triangle, TexturedRectangle
from game.sprites import BaseRectangle, BaseShape, Rectangle, Circle, Triangle, TexturedRectangle
from game.file_manager import FileManager
class Game(arcade.gui.UIView):
@@ -23,7 +23,11 @@ class Game(arcade.gui.UIView):
self.rules_box = RuleUI(self.window)
self.file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.import_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.import_file_manager.change_mode("import")
self.export_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.export_file_manager.change_mode("export")
self.ui_selector_box = self.anchor.add(arcade.gui.UIBoxLayout(vertical=False, space_between=self.window.width / 100), anchor_x="left", anchor_y="bottom", align_y=5, align_x=self.window.width / 100)
self.add_ui_selector("Simulation", lambda event: self.simulation())
@@ -40,6 +44,7 @@ class Game(arcade.gui.UIView):
self.rulesets = self.rules_box.rulesets
self.sprite_add_filemanager = FileManager(self.window.width * 0.9, self.window.height * 0.75, (0.9, 0.75), [".png", ".jpg", ".jpeg", ".bmp", ".gif"])
self.sprite_add_filemanager.change_mode("import")
self.sprite_add_ui = arcade.gui.UIBoxLayout(size_hint=(0.95, 0.9), space_between=10)
self.sprite_add_ui.add(arcade.gui.UILabel(text="Add Sprite", font_size=24, text_color=arcade.color.WHITE))
@@ -137,7 +142,7 @@ class Game(arcade.gui.UIView):
a = float(a)
if isinstance(shape, Circle):
shape.radius = a
elif isinstance(shape, Rectangle):
elif isinstance(shape, BaseRectangle):
shape.width = a
shape.height = a
elif isinstance(shape, Triangle):
@@ -167,10 +172,9 @@ class Game(arcade.gui.UIView):
self.shapes.append(Triangle(x, y, x + 10, y, x + 5, y + 10, color=arcade.color.WHITE, batch=self.shape_batch))
else:
self.shapes.append(TexturedRectangle(shape_type, img=SPRITE_TEXTURES.get(shape_type, SPRITE_TEXTURES["rectangle"]), x=x, y=y, batch=self.shape_batch))
self.shapes.append(TexturedRectangle(pyglet.image.load(self.sprite_types[shape_type]), x, y, batch=self.shape_batch, shape_type=shape_type))
shape = self.shapes[-1]
self.triggered_events.append(["spawn", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.shape_color}])
def add_sprite(self):
@@ -283,12 +287,20 @@ class Game(arcade.gui.UIView):
recurse(block)
return max_num
def dict_to_block(self, block_dict):
kwargs = block_dict.copy()
kwargs["children"] = [self.dict_to_block(child) for child in block_dict.get("children", [])]
kwargs["vars"] = [VarBlock(**var) for var in block_dict.get("vars", [])]
return Block(**kwargs)
def on_update(self, delta_time):
if self.mode == "import" and self.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "r") as file:
if self.mode == "import" and self.import_file_manager.submitted_content:
with open(self.import_file_manager.submitted_content, "r") as file:
data = json.load(file)
self.import_file_manager.submitted_content = None
self.triggered_events = []
self.rulesets = {}
@@ -296,26 +308,34 @@ class Game(arcade.gui.UIView):
self.add_widget(arcade.gui.UIMessageBox(message_text="Invalid file. Could not import rules.", width=self.window.width * 0.5, height=self.window.height * 0.25))
return
for rule_num, ruleset in data["rulesets"].items():
kwargs = ruleset
kwargs["children"] = [Block(**child) for child in ruleset["children"]]
kwargs["vars"] = [VarBlock(**var) for var in ruleset["vars"]]
block = Block(**kwargs)
self.rulesets[rule_num] = block
for rule_num, ruleset in data["rules"].items():
block = self.dict_to_block(ruleset)
self.rulesets[int(rule_num)] = block
self.sprite_types = data.get("sprites", SPRITES)
self.sprite_types = data["sprites"]
for sprite_name, sprite_path in self.sprite_types.items():
if not sprite_name in SPRITE_TEXTURES:
SPRITE_TEXTURES[sprite_name] = arcade.load_texture(sprite_path)
SPRITES[sprite_name] = sprite_path
self.sprites_grid.clear()
for n, shape in enumerate(SPRITES):
row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
self.rules_box.rulesets = self.rulesets
self.rules_box.block_renderer.blocks = self.rulesets
self.rules_box.current_rule_num = self.get_max_rule_num() + 1
self.rules_box.block_renderer.refresh()
self.rules()
if self.mode == "export" and self.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "w") as file:
if self.mode == "export" and self.export_file_manager.submitted_content:
with open(self.export_file_manager.submitted_content, "w") as file:
file.write(json.dumps(
{
"rules": {
@@ -326,6 +346,8 @@ class Game(arcade.gui.UIView):
},
indent=4))
self.export_file_manager.submitted_content = None
self.add_widget(arcade.gui.UIMessageBox(message_text="Rules and Sprites exported successfully!", width=self.window.width * 0.5, height=self.window.height * 0.25))
if not self.mode == "simulation":
@@ -342,13 +364,29 @@ class Game(arcade.gui.UIView):
self.recursive_execute_rule(rule, trigger_args)
for shape in self.shapes:
for shape_b in self.shapes:
if shape.check_collision(shape_b):
self.triggered_events.append(["collision", {"event_a_type": shape.shape_type, "event_b_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
has_collision_rules = any(
rule.rule_type == "trigger" and rule.rule == "collision"
for rule in self.rulesets.values()
)
for shape in self.shapes:
shape.update(self.x_gravity, self.y_gravity)
if has_collision_rules:
for i, shape in enumerate(self.shapes):
for shape_b in self.shapes[i+1:]:
if shape.check_collision(shape_b):
self.triggered_events.append(["collision", {
"event_a_type": shape.shape_type,
"event_b_type": shape_b.shape_type,
"shape_size": shape.shape_size,
"shape_x": shape.x,
"shape_y": shape.y,
"shape": shape,
"shape_color": shape.shape_color
}])
for shape in self.shapes[:]:
if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height:
self.destroy(shape)
@@ -386,8 +424,10 @@ class Game(arcade.gui.UIView):
self.rules_box.camera.zoom *= 1 + scroll_y * 0.1
def disable_previous(self):
if self.mode in ["import", "export"]:
self.anchor.remove(self.file_manager)
if self.mode == "import":
self.anchor.remove(self.import_file_manager)
elif self.mode == "export":
self.anchor.remove(self.export_file_manager)
elif self.mode == "rules":
self.anchor.remove(self.rules_box)
elif self.mode == "sprites":
@@ -408,17 +448,13 @@ class Game(arcade.gui.UIView):
self.disable_previous()
self.mode = "export"
self.file_manager.change_mode("export")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def import_file(self):
self.disable_previous()
self.mode = "import"
self.file_manager.change_mode("import")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def sprites(self):
self.disable_previous()

View File

@@ -31,7 +31,7 @@ class BaseShape():
def check_collision(self, other):
if isinstance(other, Circle):
return self._collides_with_circle(other)
elif isinstance(other, Rectangle):
elif isinstance(other, BaseRectangle):
return self._collides_with_rectangle(other)
elif isinstance(other, Triangle):
return self._collides_with_triangle(other)
@@ -106,7 +106,6 @@ class Circle(pyglet.shapes.Circle, BaseShape):
return not (has_neg and has_pos)
class BaseRectangle(BaseShape):
def __init__(self):
super().__init__()
@@ -182,15 +181,18 @@ class BaseRectangle(BaseShape):
class Rectangle(pyglet.shapes.Rectangle, BaseRectangle):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
BaseRectangle.__init__(self)
super().__init__(*args, **kwargs)
self.shape_type = "rectangle"
class TexturedRectangle(pyglet.sprite.Sprite, BaseRectangle):
def __init__(self, shape_type, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, img, x=0, y=0, *args, **kwargs):
BaseRectangle.__init__(self)
self.shape_type = shape_type
self.shape_type = kwargs.pop("shape_type", "textured_rectangle")
super().__init__(img, x, y, *args, **kwargs)
def update(self, x_gravity, y_gravity):
BaseShape.update(self, x_gravity, y_gravity)
class Triangle(pyglet.shapes.Triangle, BaseShape):
def __init__(self, *args, **kwargs):