Compare commits

..

3 Commits

5 changed files with 98 additions and 56 deletions

View File

@@ -42,7 +42,7 @@ jobs:
include-data-dir: assets=assets include-data-dir: assets=assets
include-data-files: CREDITS=CREDITS include-data-files: CREDITS=CREDITS
mode: onefile mode: onefile
output-file: FleetCommander output-file: ChaosProtocol
- name: Locate and rename executable (Linux) - name: Locate and rename executable (Linux)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
@@ -51,29 +51,29 @@ jobs:
echo "Searching for built Linux binary..." echo "Searching for built Linux binary..."
# List to help debugging when paths change # List to help debugging when paths change
ls -laR . | head -n 500 || true ls -laR . | head -n 500 || true
BIN=$(find . -maxdepth 4 -type f -name 'FleetCommander*' -perm -u+x | head -n1 || true) BIN=$(find . -maxdepth 4 -type f -name 'ChaosProtocol*' -perm -u+x | head -n1 || true)
if [ -z "${BIN}" ]; then if [ -z "${BIN}" ]; then
echo "ERROR: No Linux binary found after build" echo "ERROR: No Linux binary found after build"
exit 1 exit 1
fi fi
echo "Found: ${BIN}" echo "Found: ${BIN}"
mkdir -p build_output mkdir -p build_output
cp "${BIN}" build_output/FleetCommander.bin cp "${BIN}" build_output/ChaosProtocol.bin
chmod +x build_output/FleetCommander.bin chmod +x build_output/ChaosProtocol.bin
echo "Executable ready: build_output/FleetCommander.bin" echo "Executable ready: build_output/ChaosProtocol.bin"
shell: bash shell: bash
- name: Locate and rename executable (Windows) - name: Locate and rename executable (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
Write-Host "Searching for built Windows binary..." Write-Host "Searching for built Windows binary..."
Get-ChildItem -Recurse -File -Filter 'FleetCommander*.exe' | Select-Object -First 1 | ForEach-Object { Get-ChildItem -Recurse -File -Filter 'ChaosProtocol*.exe' | Select-Object -First 1 | ForEach-Object {
Write-Host ("Found: " + $_.FullName) Write-Host ("Found: " + $_.FullName)
New-Item -ItemType Directory -Force -Path build_output | Out-Null New-Item -ItemType Directory -Force -Path build_output | Out-Null
Copy-Item $_.FullName "build_output\FleetCommander.exe" Copy-Item $_.FullName "build_output\ChaosProtocol.exe"
Write-Host "Executable ready: build_output\FleetCommander.exe" Write-Host "Executable ready: build_output\ChaosProtocol.exe"
} }
if (!(Test-Path build_output\FleetCommander.exe)) { if (!(Test-Path build_output\ChaosProtocol.exe)) {
Write-Error "ERROR: No Windows binary found after build" Write-Error "ERROR: No Windows binary found after build"
exit 1 exit 1
} }
@@ -83,7 +83,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.platform }} name: ${{ matrix.platform }}
path: build_output/FleetCommander.* path: build_output/ChaosProtocol.*
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -114,5 +114,6 @@ jobs:
--notes "Automated build for $TAG" --notes "Automated build for $TAG"
fi fi
# Upload the executables directly (no zip files) # Upload the executables directly (no zip files)
gh release upload "$TAG" downloads/linux/FleetCommander.bin --clobber gh release upload "$TAG" downloads/linux/ChaosProtocol.bin --clobber
gh release upload "$TAG" downloads/windows/FleetCommander.exe --clobber gh release upload "$TAG" downloads/windows/ChaosProtocol.exe --clobber

View File

@@ -1,6 +1,9 @@
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! 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.
[![Demo Video](https://img.youtube.com/vi/iMB4mmjTIB4/hqdefault.jpg)](https://youtu.be/iMB4mmjTIB4) ## 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/iPXQfllqsvs/hqdefault.jpg)](https://youtu.be/iPXQfllqsvs)

View File

@@ -55,7 +55,7 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.submit_button.visible = self.mode == "export" self.submit_button.visible = self.mode == "export"
def submit(self, content): 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): def get_content(self, directory):
if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30: if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30:
@@ -113,10 +113,10 @@ class FileManager(arcade.gui.UIAnchorLayout):
for file in self.get_content(self.current_directory): 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,))) 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}"): if os.path.isdir(os.path.join(self.current_directory, file)):
self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(directory) self.file_buttons[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory)
else: 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): def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths if directory.startswith("//"): # Fix / paths

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 utils.constants import button_style, DO_RULES, IF_RULES, SPRITES, ALLOWED_INPUT
from game.rules import RuleUI, Block, VarBlock 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 from game.file_manager import FileManager
class Game(arcade.gui.UIView): class Game(arcade.gui.UIView):
@@ -23,7 +23,11 @@ class Game(arcade.gui.UIView):
self.rules_box = RuleUI(self.window) 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.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()) 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.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 = 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 = 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)) 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) a = float(a)
if isinstance(shape, Circle): if isinstance(shape, Circle):
shape.radius = a shape.radius = a
elif isinstance(shape, Rectangle): elif isinstance(shape, BaseRectangle):
shape.width = a shape.width = a
shape.height = a shape.height = a
elif isinstance(shape, Triangle): 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)) self.shapes.append(Triangle(x, y, x + 10, y, x + 5, y + 10, color=arcade.color.WHITE, batch=self.shape_batch))
else: 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] 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}]) 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): def add_sprite(self):
@@ -284,11 +288,19 @@ class Game(arcade.gui.UIView):
return max_num 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): def on_update(self, delta_time):
if self.mode == "import" and self.file_manager.submitted_content: if self.mode == "import" and self.import_file_manager.submitted_content:
with open(self.file_manager.submitted_content, "r") as file: with open(self.import_file_manager.submitted_content, "r") as file:
data = json.load(file) data = json.load(file)
self.import_file_manager.submitted_content = None
self.triggered_events = [] self.triggered_events = []
self.rulesets = {} 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)) 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 return
for rule_num, ruleset in data["rulesets"].items(): for rule_num, ruleset in data["rules"].items():
kwargs = ruleset block = self.dict_to_block(ruleset)
kwargs["children"] = [Block(**child) for child in ruleset["children"]] self.rulesets[int(rule_num)] = block
kwargs["vars"] = [VarBlock(**var) for var in ruleset["vars"]]
block = Block(**kwargs)
self.rulesets[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(): for sprite_name, sprite_path in self.sprite_types.items():
if not sprite_name in SPRITE_TEXTURES: if not sprite_name in SPRITE_TEXTURES:
SPRITE_TEXTURES[sprite_name] = arcade.load_texture(sprite_path) 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.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.current_rule_num = self.get_max_rule_num() + 1
self.rules_box.block_renderer.refresh() self.rules_box.block_renderer.refresh()
self.rules() self.rules()
if self.mode == "export" and self.file_manager.submitted_content: if self.mode == "export" and self.export_file_manager.submitted_content:
with open(self.file_manager.submitted_content, "w") as file: with open(self.export_file_manager.submitted_content, "w") as file:
file.write(json.dumps( file.write(json.dumps(
{ {
"rules": { "rules": {
@@ -326,6 +346,8 @@ class Game(arcade.gui.UIView):
}, },
indent=4)) 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)) 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": if not self.mode == "simulation":
@@ -342,13 +364,29 @@ class Game(arcade.gui.UIView):
self.recursive_execute_rule(rule, trigger_args) self.recursive_execute_rule(rule, trigger_args)
for shape in self.shapes: has_collision_rules = any(
for shape_b in self.shapes: rule.rule_type == "trigger" and rule.rule == "collision"
if shape.check_collision(shape_b): for rule in self.rulesets.values()
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}]) )
for shape in self.shapes:
shape.update(self.x_gravity, self.y_gravity) 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: if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height:
self.destroy(shape) self.destroy(shape)
@@ -386,8 +424,10 @@ class Game(arcade.gui.UIView):
self.rules_box.camera.zoom *= 1 + scroll_y * 0.1 self.rules_box.camera.zoom *= 1 + scroll_y * 0.1
def disable_previous(self): def disable_previous(self):
if self.mode in ["import", "export"]: if self.mode == "import":
self.anchor.remove(self.file_manager) self.anchor.remove(self.import_file_manager)
elif self.mode == "export":
self.anchor.remove(self.export_file_manager)
elif self.mode == "rules": elif self.mode == "rules":
self.anchor.remove(self.rules_box) self.anchor.remove(self.rules_box)
elif self.mode == "sprites": elif self.mode == "sprites":
@@ -408,17 +448,13 @@ class Game(arcade.gui.UIView):
self.disable_previous() self.disable_previous()
self.mode = "export" self.mode = "export"
self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
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)
def import_file(self): def import_file(self):
self.disable_previous() self.disable_previous()
self.mode = "import" self.mode = "import"
self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
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)
def sprites(self): def sprites(self):
self.disable_previous() self.disable_previous()

View File

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