From 8c451c28e7014d194d31cfb381fbf61fbff62c8c Mon Sep 17 00:00:00 2001
From: hheik <4469778+hheik@users.noreply.github.com>
Date: Mon, 8 Sep 2025 01:06:22 +0300
Subject: [PATCH] Init project.
---
godot/.editorconfig | 4 +
godot/.gitattributes | 2 +
godot/.gitignore | 3 +
godot/icon.svg | 1 +
godot/icon.svg.import | 37 +++++
godot/icons/turn_action.svg | 46 +++++
godot/icons/turn_action.svg.import | 37 +++++
godot/nodes/action.gd | 25 +++
godot/nodes/action.gd.uid | 1 +
godot/nodes/action_decider.gd | 82 +++++++++
godot/nodes/action_decider.gd.uid | 1 +
godot/nodes/actions/move_action.gd | 22 +++
godot/nodes/actions/move_action.gd.uid | 1 +
godot/nodes/actions/skip_action.gd | 6 +
godot/nodes/actions/skip_action.gd.uid | 1 +
godot/prefabs/player/player.tscn | 25 +++
godot/prefabs/player/player_input.gd | 47 ++++++
godot/prefabs/player/player_input.gd.uid | 1 +
godot/project.godot | 62 +++++++
godot/scenes/overworld.tscn | 21 +++
godot/sprites/sheet.aseprite | Bin 0 -> 2172 bytes
godot/sprites/sheet.png | Bin 0 -> 3100 bytes
godot/sprites/sheet.png.import | 34 ++++
godot/tilesets/background_tileset.tres | 13 ++
godot/tilesets/entity_tileset.tres | 13 ++
godot/tilesets/foreground_tileset.tres | 24 +++
godot/velholib.gdextension | 14 ++
godot/velholib.gdextension.uid | 1 +
rust/.gitignore | 1 +
rust/Cargo.lock | 203 +++++++++++++++++++++++
rust/Cargo.toml | 10 ++
rust/src/common.rs | 136 +++++++++++++++
rust/src/level.rs | 174 +++++++++++++++++++
rust/src/lib.rs | 10 ++
rust/src/turn_manager.rs | 58 +++++++
35 files changed, 1116 insertions(+)
create mode 100644 godot/.editorconfig
create mode 100644 godot/.gitattributes
create mode 100644 godot/.gitignore
create mode 100644 godot/icon.svg
create mode 100644 godot/icon.svg.import
create mode 100644 godot/icons/turn_action.svg
create mode 100644 godot/icons/turn_action.svg.import
create mode 100644 godot/nodes/action.gd
create mode 100644 godot/nodes/action.gd.uid
create mode 100644 godot/nodes/action_decider.gd
create mode 100644 godot/nodes/action_decider.gd.uid
create mode 100644 godot/nodes/actions/move_action.gd
create mode 100644 godot/nodes/actions/move_action.gd.uid
create mode 100644 godot/nodes/actions/skip_action.gd
create mode 100644 godot/nodes/actions/skip_action.gd.uid
create mode 100644 godot/prefabs/player/player.tscn
create mode 100644 godot/prefabs/player/player_input.gd
create mode 100644 godot/prefabs/player/player_input.gd.uid
create mode 100644 godot/project.godot
create mode 100644 godot/scenes/overworld.tscn
create mode 100644 godot/sprites/sheet.aseprite
create mode 100644 godot/sprites/sheet.png
create mode 100644 godot/sprites/sheet.png.import
create mode 100644 godot/tilesets/background_tileset.tres
create mode 100644 godot/tilesets/entity_tileset.tres
create mode 100644 godot/tilesets/foreground_tileset.tres
create mode 100644 godot/velholib.gdextension
create mode 100644 godot/velholib.gdextension.uid
create mode 100644 rust/.gitignore
create mode 100644 rust/Cargo.lock
create mode 100644 rust/Cargo.toml
create mode 100644 rust/src/common.rs
create mode 100644 rust/src/level.rs
create mode 100644 rust/src/lib.rs
create mode 100644 rust/src/turn_manager.rs
diff --git a/godot/.editorconfig b/godot/.editorconfig
new file mode 100644
index 0000000..f28239b
--- /dev/null
+++ b/godot/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+charset = utf-8
diff --git a/godot/.gitattributes b/godot/.gitattributes
new file mode 100644
index 0000000..8ad74f7
--- /dev/null
+++ b/godot/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/godot/.gitignore b/godot/.gitignore
new file mode 100644
index 0000000..0af181c
--- /dev/null
+++ b/godot/.gitignore
@@ -0,0 +1,3 @@
+# Godot 4+ specific ignores
+.godot/
+/android/
diff --git a/godot/icon.svg b/godot/icon.svg
new file mode 100644
index 0000000..9d8b7fa
--- /dev/null
+++ b/godot/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/godot/icon.svg.import b/godot/icon.svg.import
new file mode 100644
index 0000000..abdae7c
--- /dev/null
+++ b/godot/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bgacydp2nwaus"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/icons/turn_action.svg b/godot/icons/turn_action.svg
new file mode 100644
index 0000000..3802a3a
--- /dev/null
+++ b/godot/icons/turn_action.svg
@@ -0,0 +1,46 @@
+
+
+
+
diff --git a/godot/icons/turn_action.svg.import b/godot/icons/turn_action.svg.import
new file mode 100644
index 0000000..ad98fb6
--- /dev/null
+++ b/godot/icons/turn_action.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6op0q4kbyhwv"
+path="res://.godot/imported/turn_action.svg-ba28e62e6dfc65acd8d0077863718b65.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/turn_action.svg"
+dest_files=["res://.godot/imported/turn_action.svg-ba28e62e6dfc65acd8d0077863718b65.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/nodes/action.gd b/godot/nodes/action.gd
new file mode 100644
index 0000000..170ddd4
--- /dev/null
+++ b/godot/nodes/action.gd
@@ -0,0 +1,25 @@
+@icon("res://icons/turn_action.svg")
+class_name Action
+extends Node
+# Base class for actions
+
+# Optional predicate for if the action is allowed
+func predicate() -> bool:
+ return true
+
+# Called when the action starts
+func action_ready():
+ pass
+
+# Called every frame, like _process
+func action_process(_delta: float):
+ pass
+
+# Must be emitted when an action is successful
+@warning_ignore("unused_signal")
+signal done()
+# Must be emitted if something went wrong and the action can't be performed
+# This could be called for example when an action was checked to be valid, but
+# failed during execution
+@warning_ignore("unused_signal")
+signal abort()
diff --git a/godot/nodes/action.gd.uid b/godot/nodes/action.gd.uid
new file mode 100644
index 0000000..d9b32b4
--- /dev/null
+++ b/godot/nodes/action.gd.uid
@@ -0,0 +1 @@
+uid://d4ip560ei2w7n
diff --git a/godot/nodes/action_decider.gd b/godot/nodes/action_decider.gd
new file mode 100644
index 0000000..a20b103
--- /dev/null
+++ b/godot/nodes/action_decider.gd
@@ -0,0 +1,82 @@
+@icon("res://icons/turn_action.svg")
+class_name ActionDecider
+extends Node
+
+var actor: TurnActor
+var current_action: Action
+
+func get_actor():
+ var parent = get_parent()
+ if parent is TurnActor:
+ return parent
+ else:
+ return null
+
+func connect_to_actor():
+ actor.connect("turn_started", handle_turn_start)
+ actor.connect("deciding_action", handle_decide_action)
+ actor.connect("performing_action", handle_perform_action)
+ actor.connect("turn_ended", handle_turn_end)
+
+func handle_turn_start():
+ #print("turn start!")
+ pass
+
+func handle_decide_action():
+ #print("deciding action...")
+ pass
+
+func handle_perform_action():
+ #print("performing action: ", actor.get_current_action())
+ pass
+
+func handle_turn_end():
+ #print("turn end!")
+ pass
+
+func is_deciding() -> bool:
+ return actor.is_deciding()
+
+func try_perform(action_node: Action) -> bool:
+ if current_action:
+ push_error("Tried to start an action while another one is performing! Current action: ", current_action.name)
+ return false
+ add_child(action_node)
+ if action_node.predicate():
+ inner_perform(action_node)
+ return true
+ else:
+ action_node.queue_free()
+ return false
+
+func inner_perform(action_node: Action):
+ current_action = action_node
+ actor.perform_action(action_node.name)
+ current_action.done.connect(_on_action_done, CONNECT_ONE_SHOT)
+ current_action.abort.connect(_on_action_abort, CONNECT_ONE_SHOT)
+ current_action.action_ready()
+
+func _on_action_done():
+ current_action.abort.disconnect(_on_action_abort)
+ inner_action_cleanup()
+
+func _on_action_abort():
+ push_error("Action aborted: ", current_action.get_path())
+ current_action.done.disconnect(_on_action_done)
+ inner_action_cleanup()
+
+func inner_action_cleanup():
+ current_action.queue_free()
+ current_action = null
+ actor.end_turn()
+
+func _ready():
+ actor = get_actor()
+ if not actor is TurnActor:
+ push_error("Couldn't get TurnActor from TurnAction")
+ else:
+ connect_to_actor()
+
+func _process(delta: float):
+ if current_action:
+ current_action.action_process(delta)
diff --git a/godot/nodes/action_decider.gd.uid b/godot/nodes/action_decider.gd.uid
new file mode 100644
index 0000000..fcfe015
--- /dev/null
+++ b/godot/nodes/action_decider.gd.uid
@@ -0,0 +1 @@
+uid://okxdlbfuvb1b
diff --git a/godot/nodes/actions/move_action.gd b/godot/nodes/actions/move_action.gd
new file mode 100644
index 0000000..9c6300a
--- /dev/null
+++ b/godot/nodes/actions/move_action.gd
@@ -0,0 +1,22 @@
+@icon("res://icons/turn_action.svg")
+class_name MoveAction
+extends Action
+
+var mover: GridPosition
+var dir: Vector2i
+
+func _init(new_mover: GridPosition, new_dir: Vector2i) -> void:
+ mover = new_mover
+ dir = new_dir
+
+func predicate():
+ return mover.can_move(dir)
+
+func action_ready():
+ mover.finished_moving.connect(_on_mover_done, CONNECT_ONE_SHOT)
+ if not mover.try_move(dir):
+ mover.finished_moving.disconnect(_on_mover_done)
+ abort.emit()
+
+func _on_mover_done(_dir: Vector2i):
+ done.emit()
diff --git a/godot/nodes/actions/move_action.gd.uid b/godot/nodes/actions/move_action.gd.uid
new file mode 100644
index 0000000..86b587d
--- /dev/null
+++ b/godot/nodes/actions/move_action.gd.uid
@@ -0,0 +1 @@
+uid://bfsdp4v5q2jhg
diff --git a/godot/nodes/actions/skip_action.gd b/godot/nodes/actions/skip_action.gd
new file mode 100644
index 0000000..9814a02
--- /dev/null
+++ b/godot/nodes/actions/skip_action.gd
@@ -0,0 +1,6 @@
+@icon("res://icons/turn_action.svg")
+class_name SkipAction
+extends Action
+
+func action_ready():
+ done.emit()
diff --git a/godot/nodes/actions/skip_action.gd.uid b/godot/nodes/actions/skip_action.gd.uid
new file mode 100644
index 0000000..f8663bc
--- /dev/null
+++ b/godot/nodes/actions/skip_action.gd.uid
@@ -0,0 +1 @@
+uid://drnhx3nwimloi
diff --git a/godot/prefabs/player/player.tscn b/godot/prefabs/player/player.tscn
new file mode 100644
index 0000000..e598c04
--- /dev/null
+++ b/godot/prefabs/player/player.tscn
@@ -0,0 +1,25 @@
+[gd_scene load_steps=3 format=3 uid="uid://drs6h7ks4r2ta"]
+
+[ext_resource type="Texture2D" uid="uid://doscvutq8uqmd" path="res://sprites/sheet.png" id="1_72ieh"]
+[ext_resource type="Script" uid="uid://sxo578w2yds2" path="res://prefabs/player/player_input.gd" id="2_rdx4y"]
+
+[node name="Player" type="TurnActor"]
+
+[node name="Input" type="Node2D" parent="."]
+script = ExtResource("2_rdx4y")
+metadata/_custom_type_script = "uid://okxdlbfuvb1b"
+
+[node name="GridPosition" type="GridPosition" parent="."]
+
+[node name="Sprite2D" type="Sprite2D" parent="GridPosition"]
+texture = ExtResource("1_72ieh")
+centered = false
+hframes = 8
+vframes = 8
+frame = 1
+
+[node name="Camera2D" type="Camera2D" parent="GridPosition"]
+position = Vector2(16, 16)
+zoom = Vector2(2, 2)
+
+[node name="Gatherer" type="Node" parent="GridPosition"]
diff --git a/godot/prefabs/player/player_input.gd b/godot/prefabs/player/player_input.gd
new file mode 100644
index 0000000..93fb811
--- /dev/null
+++ b/godot/prefabs/player/player_input.gd
@@ -0,0 +1,47 @@
+@icon("res://icons/turn_action.svg")
+extends ActionDecider
+
+@onready var grid_position: GridPosition = $"../GridPosition"
+
+func poll_movement_dir():
+ var movement_dir = Vector2i.ZERO
+ if Input.is_action_pressed("move_up"):
+ movement_dir = Vector2i.UP
+ elif Input.is_action_pressed("move_down"):
+ movement_dir = Vector2i.DOWN
+ elif Input.is_action_pressed("move_left"):
+ movement_dir = Vector2i.LEFT
+ elif Input.is_action_pressed("move_right"):
+ movement_dir = Vector2i.RIGHT
+ return movement_dir
+
+func _input(event: InputEvent) -> void:
+ if not is_deciding():
+ return
+ if event is InputEventKey:
+ if event.is_pressed():
+ match event.keycode:
+ KEY_SPACE:
+ print("skip turn!")
+ if try_perform(SkipAction.new()):
+ return
+ #KEY_UP:
+ #if try_perform(MoveAction.new(grid_position, Vector2i.UP)):
+ #return
+ #KEY_DOWN:
+ #if try_perform(MoveAction.new(grid_position, Vector2i.DOWN)):
+ #return
+ #KEY_LEFT:
+ #if try_perform(MoveAction.new(grid_position, Vector2i.LEFT)):
+ #return
+ #KEY_RIGHT:
+ #if try_perform(MoveAction.new(grid_position, Vector2i.RIGHT)):
+ #return
+
+func _process(_delta: float) -> void:
+ if not is_deciding():
+ return
+ var movement_dir = poll_movement_dir()
+ if movement_dir != Vector2i.ZERO:
+ if try_perform(MoveAction.new(grid_position, movement_dir)):
+ return
diff --git a/godot/prefabs/player/player_input.gd.uid b/godot/prefabs/player/player_input.gd.uid
new file mode 100644
index 0000000..e81df58
--- /dev/null
+++ b/godot/prefabs/player/player_input.gd.uid
@@ -0,0 +1 @@
+uid://sxo578w2yds2
diff --git a/godot/project.godot b/godot/project.godot
new file mode 100644
index 0000000..3a5d453
--- /dev/null
+++ b/godot/project.godot
@@ -0,0 +1,62 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Velho Peli"
+run/main_scene="uid://b3odri2wtvke4"
+config/features=PackedStringArray("4.4", "Forward Plus")
+config/icon="res://icon.svg"
+
+[display]
+
+window/size/viewport_width=1216
+window/size/viewport_height=704
+window/vsync/vsync_mode=0
+
+[dotnet]
+
+project/assembly_name="Velho Peli"
+
+[input]
+
+move_up={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_down={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_left={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+move_right={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+skip_turn={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
+]
+}
+
+[rendering]
+
+textures/canvas_textures/default_texture_filter=0
diff --git a/godot/scenes/overworld.tscn b/godot/scenes/overworld.tscn
new file mode 100644
index 0000000..75f56c0
--- /dev/null
+++ b/godot/scenes/overworld.tscn
@@ -0,0 +1,21 @@
+[gd_scene load_steps=4 format=4 uid="uid://b3odri2wtvke4"]
+
+[ext_resource type="TileSet" uid="uid://b1ps1ww0rtkop" path="res://tilesets/background_tileset.tres" id="1_m1b5j"]
+[ext_resource type="TileSet" uid="uid://bxwohuw2p43k1" path="res://tilesets/foreground_tileset.tres" id="2_u2ss0"]
+[ext_resource type="PackedScene" uid="uid://drs6h7ks4r2ta" path="res://prefabs/player/player.tscn" id="3_u2ss0"]
+
+[node name="Root" type="Level" node_paths=PackedStringArray("background", "foreground")]
+background = NodePath("Background")
+foreground = NodePath("Foreground")
+
+[node name="Background" type="TileMapLayer" parent="."]
+tile_map_data = PackedByteArray("AAD8//z/AQAAAAQAAAD8//3/AQAAAAQAAAD8//7/AQAAAAQAAAD8////AQAAAAQAAAD9//z/AQAAAAQAAAD9//3/AQAAAAQAAAD9//7/AQAAAAQAAAD9////AQAAAAQAAAD+//z/AQAAAAQAAAD+//3/AQAAAAQAAAD+//7/AQAAAAQAAAD+////AQAAAAQAAAD///z/AQAAAAQAAAD///3/AQAAAAQAAAD///7/AQAAAAQAAAD/////AQAAAAQAAAD8/wAAAQAAAAQAAAD8/wEAAQAAAAQAAAD8/wIAAQAAAAQAAAD8/wMAAQAAAAQAAAD9/wAAAQAAAAQAAAD9/wEAAQAAAAQAAAD9/wIAAQAAAAQAAAD9/wMAAQAAAAQAAAD+/wAAAQAAAAQAAAD+/wEAAQAAAAQAAAD+/wIAAQAAAAQAAAD+/wMAAQAAAAQAAAD//wAAAQAAAAQAAAD//wEAAQAAAAQAAAD//wIAAQAAAAQAAAD//wMAAQAAAAQAAAAAAPz/AQAAAAQAAAAAAP3/AQAAAAQAAAAAAP7/AQAAAAQAAAAAAP//AQAAAAQAAAAAAAAAAQAAAAQAAAAAAAEAAQAAAAQAAAAAAAIAAQAAAAQAAAAAAAMAAQAAAAQAAAABAPz/AQAAAAQAAAABAP3/AQAAAAQAAAABAP7/AQAAAAQAAAABAP//AQAAAAQAAAABAAAAAQAAAAQAAAABAAEAAQAAAAQAAAABAAIAAQAAAAQAAAABAAMAAQAAAAQAAAACAPz/AQAAAAQAAAACAP3/AQAAAAQAAAACAP7/AQAAAAQAAAACAP//AQAAAAQAAAACAAAAAQAAAAQAAAACAAEAAQAAAAQAAAACAAIAAQAAAAQAAAACAAMAAQAAAAQAAAADAPz/AQAAAAQAAAADAP3/AQAAAAQAAAADAP7/AQAAAAQAAAADAP//AQAAAAQAAAADAAAAAQAAAAQAAAADAAEAAQAAAAQAAAADAAIAAQAAAAQAAAADAAMAAQAAAAQAAAD6//v/AQAAAAQAAAD6//z/AQAAAAQAAAD6//3/AQAAAAQAAAD6//7/AQAAAAQAAAD6////AQAAAAQAAAD6/wAAAQAAAAQAAAD6/wEAAQAAAAQAAAD6/wIAAQAAAAQAAAD6/wMAAQAAAAQAAAD6/wQAAQAAAAQAAAD6/wUAAQAAAAQAAAD7//v/AQAAAAQAAAD7//z/AQAAAAQAAAD7//3/AQAAAAQAAAD7//7/AQAAAAQAAAD7////AQAAAAQAAAD7/wAAAQAAAAQAAAD7/wEAAQAAAAQAAAD7/wIAAQAAAAQAAAD7/wMAAQAAAAQAAAD7/wQAAQAAAAQAAAD7/wUAAQAAAAQAAAD8//v/AQAAAAQAAAD8/wQAAQAAAAQAAAD8/wUAAQAAAAQAAAD9//v/AQAAAAQAAAD9/wQAAQAAAAQAAAD9/wUAAQAAAAQAAAD+//v/AQAAAAQAAAD+/wQAAQAAAAQAAAD+/wUAAQAAAAQAAAD///v/AQAAAAQAAAD//wQAAQAAAAQAAAD//wUAAQAAAAQAAAAAAPv/AQAAAAQAAAAAAAQAAQAAAAQAAAAAAAUAAQAAAAQAAAABAPv/AQAAAAQAAAABAAQAAQAAAAQAAAABAAUAAQAAAAQAAAACAPv/AQAAAAQAAAACAAQAAQAAAAQAAAACAAUAAQAAAAQAAAADAPr/AQABAAQAAAADAPv/AQAAAAQAAAADAAQAAQAAAAQAAAADAAUAAQAAAAQAAAAEAPr/AQABAAQAAAAEAPv/AQABAAQAAAAEAPz/AQABAAQAAAAEAP3/AQABAAQAAAAEAP7/AQAAAAQAAAAEAP//AQAAAAQAAAAEAAAAAQAAAAQAAAAEAAEAAQAAAAQAAAAEAAIAAQAAAAQAAAAEAAMAAQAAAAQAAAAEAAQAAQAAAAQAAAAEAAUAAQAAAAQAAAAFAPr/AQABAAQAAAAFAPv/AQABAAQAAAAFAPz/AQABAAQAAAAFAP3/AQABAAQAAAAFAP7/AQAAAAQAAAAFAP//AQAAAAQAAAAFAAAAAQAAAAQAAAAFAAEAAQAAAAQAAAAFAAIAAQAAAAQAAAAFAAMAAQAAAAQAAAAFAAQAAQAAAAQAAAAFAAUAAQAAAAQAAAAGAPj/AQABAAQAAAAGAPn/AQABAAQAAAAGAPr/AQABAAQAAAAGAPv/AQABAAQAAAAGAPz/AQABAAQAAAAHAPj/AQABAAQAAAAHAPn/AQABAAQAAAAHAPr/AQABAAQAAAAHAPv/AQABAAQAAAAHAPz/AQABAAQAAAAIAPj/AQABAAQAAAAIAPn/AQABAAQAAAAIAPr/AQABAAQAAAAIAPv/AQABAAQAAAAIAPz/AQABAAQAAAAJAPj/AQABAAQAAAAJAPn/AQABAAQAAAAJAPr/AQABAAQAAAAJAPv/AQABAAQAAAAJAPz/AQABAAQAAAAKAPj/AQABAAQAAAAKAPn/AQABAAQAAAAKAPr/AQABAAQAAAAKAPv/AQABAAQAAAAKAPz/AQABAAQAAAAEAPn/AQABAAQAAAAGAP3/AQABAAQAAAAHAP3/AQABAAQAAAAIAP3/AQABAAQAAAAIAP7/AQABAAQAAAAHAP7/AQABAAQAAAAEAPj/AQABAAQAAAAEAPf/AQABAAQAAAAFAPf/AQABAAQAAAAFAPj/AQABAAQAAAAFAPn/AQABAAQAAAAHAPf/AQABAAQAAAAGAPf/AQABAAQAAAAIAPf/AQABAAQAAAAJAPf/AQABAAQAAAAKAPf/AQABAAQAAAALAPn/AQABAAQAAAALAPr/AQABAAQAAAALAPv/AQABAAQAAAAKAP3/AQABAAQAAAAJAP3/AQABAAQAAAD3//v/AQAAAAQAAAD3//z/AQAAAAQAAAD3//3/AQAAAAQAAAD3//7/AQAAAAQAAAD3////AQAAAAQAAAD3/wAAAQAAAAQAAAD3/wEAAQAAAAQAAAD3/wIAAQAAAAQAAAD3/wMAAQAAAAQAAAD3/wQAAQAAAAQAAAD3/wUAAQAAAAQAAAD4//v/AQAAAAQAAAD4//z/AQAAAAQAAAD4//3/AQAAAAQAAAD4//7/AQAAAAQAAAD4////AQAAAAQAAAD4/wAAAQAAAAQAAAD4/wEAAQAAAAQAAAD4/wIAAQAAAAQAAAD4/wMAAQAAAAQAAAD4/wQAAQAAAAQAAAD4/wUAAQAAAAQAAAD5//v/AQAAAAQAAAD5//z/AQAAAAQAAAD5//3/AQAAAAQAAAD5//7/AQAAAAQAAAD5////AQAAAAQAAAD5/wAAAQAAAAQAAAD5/wEAAQAAAAQAAAD5/wIAAQAAAAQAAAD5/wMAAQAAAAQAAAD5/wQAAQAAAAQAAAD5/wUAAQAAAAQAAAAGAP//AQAAAAQAAAAGAAAAAQAAAAQAAAAGAAEAAQAAAAQAAAAGAAIAAQAAAAQAAAAGAAMAAQAAAAQAAAAGAAQAAQAAAAQAAAAGAAUAAQAAAAQAAAAHAP//AQAAAAQAAAAHAAAAAQAAAAQAAAAHAAEAAQAAAAQAAAAHAAIAAQAAAAQAAAAHAAMAAQAAAAQAAAAHAAQAAQAAAAQAAAAHAAUAAQAAAAQAAAAIAP//AQAAAAQAAAAIAAAAAQAAAAQAAAAIAAEAAQAAAAQAAAAIAAIAAQAAAAQAAAAIAAMAAQAAAAQAAAAIAAQAAQAAAAQAAAAIAAUAAQAAAAQAAAAJAP//AQAAAAQAAAAJAAAAAQAAAAQAAAAJAAEAAQAAAAQAAAAJAAIAAQAAAAQAAAAJAAMAAQAAAAQAAAAJAAQAAQAAAAQAAAAJAAUAAQAAAAQAAAAJAP7/AQABAAQAAAAGAP7/AQABAAQAAAA=")
+tile_set = ExtResource("1_m1b5j")
+
+[node name="Foreground" type="TileMapLayer" parent="."]
+tile_map_data = PackedByteArray("AAD/////AQAAAAMAAAAEAAEAAQAAAAIAAAADAAEAAQAAAAIAAAADAAIAAQAAAAIAAAADAAMAAQAAAAIAAAACAAMAAQAAAAIAAAAFAAMAAQABAAIAAAAGAAMAAQABAAIAAAD8//3/AQABAAMAAAD9//3/AQABAAMAAAD9//7/AQABAAMAAAD9/wAAAQABAAMAAAD9/wEAAQABAAMAAAD8/wEAAQABAAMAAAAEAPv/AQACAAMAAAAGAP3/AQACAAMAAAAHAP7/AQACAAMAAAAIAP7/AQACAAMAAAAJAP7/AQACAAMAAAAGAPn/AQACAAMAAAAGAPr/AQACAAMAAAAGAPv/AQACAAMAAAAHAPv/AQACAAMAAAAIAPv/AQACAAMAAAAIAPz/AQACAAMAAAAJAPz/AQACAAMAAAAKAPz/AQACAAMAAAAKAPv/AQACAAMAAAAJAPv/AQACAAMAAAAIAPr/AQACAAMAAAAHAPr/AQACAAMAAAAHAPn/AQACAAMAAAD3//v/AQABAAMAAAD3//z/AQABAAMAAAD3//3/AQABAAMAAAD3//7/AQABAAMAAAD3////AQABAAMAAAD3/wAAAQABAAMAAAD3/wEAAQABAAMAAAD3/wIAAQABAAMAAAD4//v/AQABAAMAAAD4//z/AQABAAMAAAD4//3/AQABAAMAAAD4//7/AQABAAMAAAD4////AQABAAMAAAD4/wAAAQABAAMAAAD4/wEAAQABAAMAAAD4/wIAAQABAAMAAAD3/wMAAQABAAMAAAD3/wQAAQABAAMAAAD3/wUAAQABAAMAAAD4/wMAAQABAAMAAAD4/wQAAQABAAMAAAD4/wUAAQABAAMAAAD5/wUAAQABAAMAAAD6/wUAAQABAAMAAAD7/wUAAQABAAMAAAD8/wUAAQABAAMAAAD9/wUAAQABAAMAAAD+/wUAAQABAAMAAAD//wUAAQABAAMAAAAAAAUAAQABAAMAAAABAAUAAQABAAMAAAACAAUAAQABAAMAAAADAAUAAQABAAMAAAAEAAUAAQABAAMAAAAFAAUAAQABAAMAAAAGAAUAAQABAAMAAAAHAAUAAQABAAMAAAAIAAUAAQABAAMAAAAJAAUAAQABAAMAAAD5//v/AQABAAMAAAD6//v/AQABAAMAAAD7//v/AQABAAMAAAD8//v/AQABAAMAAAD9//v/AQABAAMAAAD+//v/AQABAAMAAAD///v/AQABAAMAAAAAAPv/AQABAAMAAAABAPv/AQABAAMAAAACAPv/AQABAAMAAAADAPv/AQABAAMAAAAJAP//AQABAAMAAAAJAAAAAQABAAMAAAAJAAEAAQABAAMAAAAJAAIAAQABAAMAAAAJAAMAAQABAAMAAAAJAAQAAQABAAMAAAD7/wEAAQABAAMAAAD7//3/AQABAAMAAAD6//3/AQABAAMAAAD6/wEAAQABAAMAAAD6//7/AQABAAMAAAD6/wAAAQABAAMAAAA=")
+tile_set = ExtResource("2_u2ss0")
+
+[node name="Player" parent="." instance=ExtResource("3_u2ss0")]
+
+[node name="TurnManager" type="TurnManager" parent="."]
diff --git a/godot/sprites/sheet.aseprite b/godot/sprites/sheet.aseprite
new file mode 100644
index 0000000000000000000000000000000000000000..e304de237bf3443cf258d19a1316e1dc7a21d610
GIT binary patch
literal 2172
zcmcIkeNO`7erm4hUbkS8$1ofhsTovV(D8xtdv{|e7{(ft{eb(M*pS|~4zkSYL
z>&KacLiJV6LZN1%W?`lc3Uy>!|098@Su;VD`}F#Mh}{{5vc5xqL`6>nESy$n%ty%o
zg5yp5=$Y;UryYOHnvE+WjuEl^n04B;0CnXt!{ckV@$j<29mzll{>sU%Nx{3eE(?ty
zlF6q-5^@im?5CAG;$OUZ-fed{9ObprAG7n4l9OX&b}_%bP!k%e-b+)loA@PLE}RPa
z1RCHoJ7vfGIau^JtYv;P@EE60ITXqV9-!L09lXXHlKAYKRgo_fQ
zG6Z9S7k7O(x}T$0(A~W*pFN4*;7*Cjy|8EyvjUC(cH$fK-_RRFFtPb}7Hn{r^wmVZ
zo%`fKTlA5E)dh;5e69N7iV?$zp{&~cy}t#t9J_m4G*w>ae#1Xt)uxcOvhb_>Wi0cf
zJ5790Qd9+n#kWj7K0k9PJX7#uGJL~_WYSvMrtnG8^|5{B5ozb_U#nmBd_})?|K)&L
z(uryOtCWhPCnYqm*5ZC=j%2^FYu<`)-s%+HV`2gH$NRYhtFX!~h4md4TwUi|lZokS
zR8JKP_Pf;H))S}fmWwz=qD{HUf~$XJcvc*^wN%#4yBsFDe4?dqH*1szcg|wD05Oi1f
zd-E;KiWXd(MA+4mSiv|~gPxA})?M+nSOQ<9f
z*zPp>q!Wpu+jLaHOXnZcog5J7uwA^7LiuZ`A8jgOs*w?BQ^!2RCXf|V={~w_zfre=
zp9Zk-!vvW(Z)J%IsS-Lq4A6HEF-BRH^Sa~P9@>nZU=`qEa+NRVRJafjIn^|l3RMW!
z$GP-ze1H%8CG7gr-S0_(sbV(*@)#0HZ9RWVBTQl6Q5sIT2+Xtzpce&mHxrSL&k3r{
zql;Y#A4^rv0{s!DtF|eJ@u|f%pF5nd1>N$J01$dGGm1x?Pt}$|beqcpT^TiFz0A@-
z=CO_Uh~_C@-d&Vt|7xfMz6q2;#qv;15LW3Uq^baCxXP#~>&y{xNu}~yqAF9@v_k`f
zYrG~5#*V3{HT2=MXL{E-=@aNsaKVt#1H1`uZdlG1RmVk{TJMnJ_3JW3ZCJ>}&|uzh
zf^9iPT%gmO<-0}2$QslcJgS>l!hj}3)1?w-zy6lQG&t!f--G)0R90N
zXn!=PuG7^1Ve7_TwSH|N|BAVX6i=O65(zU5(5)NLBb*u!a%G;T=zDr)hA*(UPG|Cf
z_he|VUQ^`{lZwb);vRi)8#ebyZ8B$fR-tF?^Ht+f^qU<-`8uMp*7IMM-2hDwL~Kf%WUC~y!cREENvOWj1lcbahx-%vzLfBxbF9TRK=ckCl9(Bgj$Wu08$sxH
ziGeK3zhudunE1UCxdeI1eh^7LV#Q?3_``Z-*sn;bu@$-ru$vEDPZURU9Pg#ehd7WL
zSdLDt-Hb12)or>L(Bh6UZsrgvKdPe4Ntc7b&twc=07`xL3bVesmBjH>#_Y
zje5%okny@>yG*hnFwIvZYXlD>XDv~wX)l6hS;(JD0~OdkLxOrycSrhqx59Vr_LGj9
z8?G&l-t}1cy7oIl{CHk-`8}?~+ISIjxmQ}Umc@%vUlJ8CfrcgHK~MEPUli$8BZ9EJ
zyj--QI7y%W&Tjd^D-QNJVY-2ZtFtj(;?CcMw{tad#*v;3FGVqDVX^1wN;otsos^g0
zyAyW)TF+(zAecF!T5sgO!pQxWsLxHWz9!
zHjR-Iwe#VlDgD-kawKD;0Rp{;=I+A_A}1=EJEBzX;750x1}xGWteTu<+j)HoKQIgJdeA!@H{KKQ3Sp3
z>mBZwYUkyb^U3?i`C`YMzBs=b_L-p+Cvb8WY|UYC|7axr&tck?Qv|=fYRCCfI!Tqi
V=lB^?yj16X(6PYo=0Vib{|$pyN}&J%
literal 0
HcmV?d00001
diff --git a/godot/sprites/sheet.png b/godot/sprites/sheet.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0f4ce9fd4a2b5a546e967094855c01a22dc35e9
GIT binary patch
literal 3100
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
ahxVb@d=0?!vSow=JSa,
+ base: Base,
+}
+
+#[godot_api]
+impl INode2D for TurnActor {
+ fn ready(&mut self) {}
+}
+
+#[godot_api]
+impl TurnActor {
+ #[func]
+ pub fn get_state(&self) -> TurnActorState {
+ self.state
+ }
+
+ #[func]
+ pub fn get_current_action(&self) -> Variant {
+ match self.current_action.clone() {
+ Some(action) => action.to_variant(),
+ None => Variant::nil(),
+ }
+ }
+
+ #[func]
+ pub fn is_deciding(&self) -> bool {
+ self.state == TurnActorState::DecidingAction
+ }
+
+ #[func]
+ pub fn is_my_turn(&self) -> bool {
+ match self.state {
+ TurnActorState::TurnStarted
+ | TurnActorState::DecidingAction
+ | TurnActorState::PerformingAction => true,
+ TurnActorState::WaitingForTurn => false,
+ }
+ }
+
+ /// Should be called by a turn manager
+ #[func]
+ pub fn start_turn(&mut self) {
+ if self.state != TurnActorState::WaitingForTurn {
+ godot_error!(
+ "TurnActor: incorrect state transfer. Called 'start_turn' while the actor is in state {:?} (expected WaitingForTurn)",
+ self.state,
+ );
+ }
+ self.state = TurnActorState::TurnStarted;
+ self.signals().turn_started().emit();
+ self.start_deciding();
+ }
+
+ /// Currently called automatically after the turn is started
+ #[func]
+ pub fn start_deciding(&mut self) {
+ if self.state != TurnActorState::TurnStarted {
+ godot_error!(
+ "TurnActor: incorrect state transfer. Called 'start_deciding' while the actor is in state {:?} (expected TurnStarted)",
+ self.state,
+ );
+ }
+ self.state = TurnActorState::DecidingAction;
+ self.signals().deciding_action().emit();
+ }
+
+ /// Should be called by an action script.
+ #[func]
+ pub fn perform_action(&mut self, action_name: GString) {
+ if self.state != TurnActorState::DecidingAction {
+ godot_error!(
+ "TurnActor: incorrect state transfer. Called 'take_control(\"{action_name}\")' while the actor is in state {:?} (expected DecidingAction)",
+ self.state,
+ );
+ }
+ self.state = TurnActorState::PerformingAction;
+ self.current_action = Some(action_name);
+ self.signals().performing_action().emit();
+ }
+
+ /// Should be called by an action script after it's done.
+ #[func]
+ pub fn end_turn(&mut self) {
+ if self.state != TurnActorState::PerformingAction {
+ godot_error!(
+ "TurnActor: incorrect state transfer. Called 'end_turn' while the actor is in state {:?} (expected PerformingAction)",
+ self.state,
+ );
+ }
+ self.state = TurnActorState::WaitingForTurn;
+ self.current_action = None;
+ self.signals().turn_ended().emit();
+ }
+
+ #[signal]
+ pub fn turn_started();
+
+ #[signal]
+ pub fn deciding_action();
+
+ #[signal]
+ pub fn performing_action();
+
+ #[signal]
+ pub fn turn_ended();
+}
diff --git a/rust/src/level.rs b/rust/src/level.rs
new file mode 100644
index 0000000..e642594
--- /dev/null
+++ b/rust/src/level.rs
@@ -0,0 +1,174 @@
+use godot::{classes::*, prelude::*};
+
+#[derive(Debug, GodotClass)]
+#[class(init, base=Node)]
+pub struct Level {
+ #[export]
+ background: Option>,
+ #[export]
+ foreground: Option>,
+
+ base: Base,
+}
+
+#[godot_api]
+impl INode for Level {
+ fn ready(&mut self) {
+ if self.background.is_none() {
+ self.background = self.base().try_get_node_as::("./Background");
+ }
+ if self.foreground.is_none() {
+ self.foreground = self.base().try_get_node_as::("./Foreground");
+ }
+ }
+}
+
+#[godot_api]
+impl Level {
+ const HAS_COLLISION: &str = "has_collision";
+
+ #[func]
+ pub fn find_from_node(node: Gd) -> Option> {
+ let mut current = node.clone();
+ while let Some(parent) = current.get_parent() {
+ match parent.try_cast::() {
+ Ok(level) => return Some(level),
+ Err(other) => current = other,
+ }
+ }
+ None
+ }
+
+ #[func]
+ pub fn tile_has_collision(&self, coords: Vector2i) -> bool {
+ self.foreground
+ .as_ref()
+ .is_some_and(|layer| get_custom_data_bool(layer, coords, Self::HAS_COLLISION))
+ || self
+ .background
+ .as_ref()
+ .is_some_and(|layer| get_custom_data_bool(layer, coords, Self::HAS_COLLISION))
+ }
+
+ #[func]
+ pub fn get_fg(&self) -> Gd {
+ self.foreground.clone().unwrap()
+ }
+}
+
+fn get_custom_data_bool(layer: &Gd, coords: Vector2i, custom_data: &str) -> bool {
+ layer.get_cell_tile_data(coords).is_some_and(|tile| {
+ tile.has_custom_data(custom_data) && tile.get_custom_data(custom_data).booleanize()
+ })
+}
+
+#[derive(Debug, GodotClass)]
+#[class(init, base=Node2D)]
+pub struct GridPosition {
+ #[export]
+ #[init(val = Vector2::splat(32.0))]
+ grid_size: Vector2,
+
+ #[export]
+ #[init(val = 30.0)]
+ movement_speed: f32,
+
+ #[export_group(name = "Flags")]
+ #[export]
+ #[init(val = false)]
+ ignore_collisions: bool,
+
+ is_moving: bool,
+ target_coords: Vector2i,
+
+ base: Base,
+}
+
+#[godot_api]
+impl INode2D for GridPosition {
+ fn ready(&mut self) {
+ self.target_coords = self.get_coords();
+ }
+
+ fn process(&mut self, delta: f64) {
+ if self.is_moving {
+ let start = self.base().get_global_position();
+ let target_coords = self.target_coords;
+ let movement_speed = self.movement_speed;
+ let end = self.get_target_pos();
+ if start.distance_squared_to(end) <= Self::TILE_SNAP_DIST_SQR {
+ self.base_mut().set_global_position(end);
+ self.is_moving = false;
+ self.signals().finished_moving().emit(target_coords);
+ } else {
+ // self.base_mut()
+ // .set_global_position(start.move_toward(end, movement_speed * delta));
+ self.base_mut()
+ .set_global_position(start.lerp(end, movement_speed * delta as f32));
+ }
+ }
+ }
+}
+
+#[godot_api]
+impl GridPosition {
+ const TILE_SNAP_DIST_SQR: f32 = 1.0;
+
+ fn level(&self) -> Option> {
+ Level::find_from_node(self.to_gd().upcast::())
+ }
+
+ #[func]
+ pub fn get_target_pos(&self) -> Vector2 {
+ self.target_coords.cast_float() * self.grid_size
+ }
+
+ #[func]
+ pub fn get_coords(&self) -> Vector2i {
+ (self.base().get_global_position() / self.grid_size)
+ .round()
+ .cast_int()
+ }
+
+ #[func]
+ pub fn set_coords(&mut self, pos: Vector2i) {
+ let grid_size = self.grid_size;
+ self.base_mut()
+ .set_global_position(pos.cast_float() * grid_size);
+ }
+
+ #[func]
+ pub fn can_move(&self, dir: Vector2i) -> bool {
+ if self.is_moving {
+ return false;
+ }
+ if self.ignore_collisions {
+ return true;
+ }
+ match self.level() {
+ Some(level) => !level.bind().tile_has_collision(self.get_coords() + dir),
+ None => true,
+ }
+ }
+
+ #[func]
+ pub fn try_move(&mut self, dir: Vector2i) -> bool {
+ let start_coords = self.target_coords;
+ let target_coords = start_coords + dir;
+
+ if !self.can_move(dir) {
+ return false;
+ }
+
+ self.is_moving = true;
+ self.target_coords = target_coords;
+ self.signals().started_moving().emit(start_coords, dir);
+ true
+ }
+
+ #[signal]
+ fn started_moving(from_coords: Vector2i, dir: Vector2i);
+
+ #[signal]
+ fn finished_moving(coords: Vector2i);
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644
index 0000000..494cbd2
--- /dev/null
+++ b/rust/src/lib.rs
@@ -0,0 +1,10 @@
+use godot::prelude::*;
+
+pub struct VelhoExtension;
+
+mod common;
+mod level;
+mod turn_manager;
+
+#[gdextension]
+unsafe impl ExtensionLibrary for VelhoExtension {}
diff --git a/rust/src/turn_manager.rs b/rust/src/turn_manager.rs
new file mode 100644
index 0000000..5ae4eeb
--- /dev/null
+++ b/rust/src/turn_manager.rs
@@ -0,0 +1,58 @@
+use godot::{
+ classes::{object::ConnectFlags, *},
+ prelude::*,
+};
+
+use crate::common::TurnActor;
+
+#[derive(Debug, GodotClass)]
+#[class(init, base=Node)]
+pub struct TurnManager {
+ round_queue: Array>,
+ current_actor: Option>,
+
+ base: Base,
+}
+
+#[godot_api]
+impl INode for TurnManager {
+ fn process(&mut self, _delta: f64) {
+ if self.current_actor.is_none() && self.round_queue.is_empty() {
+ self.start_round();
+ }
+ }
+}
+
+#[godot_api]
+impl TurnManager {
+ fn start_round(&mut self) {
+ self.round_queue = self.find_sibling_actors();
+ godot_print!("New round: {:?}", self.round_queue);
+ self.start_next_turn();
+ }
+
+ fn start_next_turn(&mut self) {
+ self.current_actor = self.round_queue.pop();
+ if let Some(mut actor) = self.current_actor.clone() {
+ actor
+ .signals()
+ .turn_ended()
+ .builder()
+ .flags(ConnectFlags::ONE_SHOT)
+ .connect_other_mut(self, |this| this.start_next_turn());
+ actor.bind_mut().start_turn();
+ }
+ }
+
+ fn find_sibling_actors(&self) -> Array> {
+ let mut actors: Array> = Array::new();
+ self.base()
+ .get_parent()
+ .unwrap()
+ .get_children()
+ .iter_shared()
+ .filter_map(|node| node.try_cast::().ok())
+ .for_each(|actor| actors.push(&actor));
+ actors
+ }
+}