commit 8c451c28e7014d194d31cfb381fbf61fbff62c8c
Author: hheik <4469778+hheik@users.noreply.github.com>
Date: Mon Sep 8 01:06:22 2025 +0300
Init project.
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 0000000..e304de2
Binary files /dev/null and b/godot/sprites/sheet.aseprite differ
diff --git a/godot/sprites/sheet.png b/godot/sprites/sheet.png
new file mode 100644
index 0000000..c0f4ce9
Binary files /dev/null and b/godot/sprites/sheet.png differ
diff --git a/godot/sprites/sheet.png.import b/godot/sprites/sheet.png.import
new file mode 100644
index 0000000..e88316f
--- /dev/null
+++ b/godot/sprites/sheet.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://doscvutq8uqmd"
+path="res://.godot/imported/sheet.png-611d8b977306b2c8b91271f64fa10ad9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://sprites/sheet.png"
+dest_files=["res://.godot/imported/sheet.png-611d8b977306b2c8b91271f64fa10ad9.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
diff --git a/godot/tilesets/background_tileset.tres b/godot/tilesets/background_tileset.tres
new file mode 100644
index 0000000..e9b12be
--- /dev/null
+++ b/godot/tilesets/background_tileset.tres
@@ -0,0 +1,13 @@
+[gd_resource type="TileSet" load_steps=3 format=3 uid="uid://b1ps1ww0rtkop"]
+
+[ext_resource type="Texture2D" uid="uid://doscvutq8uqmd" path="res://sprites/sheet.png" id="1_ge1ul"]
+
+[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_5boi2"]
+texture = ExtResource("1_ge1ul")
+texture_region_size = Vector2i(32, 32)
+0:4/0 = 0
+1:4/0 = 0
+
+[resource]
+tile_size = Vector2i(32, 32)
+sources/1 = SubResource("TileSetAtlasSource_5boi2")
diff --git a/godot/tilesets/entity_tileset.tres b/godot/tilesets/entity_tileset.tres
new file mode 100644
index 0000000..327f216
--- /dev/null
+++ b/godot/tilesets/entity_tileset.tres
@@ -0,0 +1,13 @@
+[gd_resource type="TileSet" load_steps=3 format=3 uid="uid://7lfy0ix4n65n"]
+
+[ext_resource type="Texture2D" uid="uid://doscvutq8uqmd" path="res://sprites/sheet.png" id="1_1tjj6"]
+
+[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_yenu7"]
+texture = ExtResource("1_1tjj6")
+texture_region_size = Vector2i(32, 32)
+0:0/0 = 0
+1:0/0 = 0
+
+[resource]
+tile_size = Vector2i(32, 32)
+sources/0 = SubResource("TileSetAtlasSource_yenu7")
diff --git a/godot/tilesets/foreground_tileset.tres b/godot/tilesets/foreground_tileset.tres
new file mode 100644
index 0000000..b8f7a9e
--- /dev/null
+++ b/godot/tilesets/foreground_tileset.tres
@@ -0,0 +1,24 @@
+[gd_resource type="TileSet" load_steps=3 format=3 uid="uid://bxwohuw2p43k1"]
+
+[ext_resource type="Texture2D" uid="uid://doscvutq8uqmd" path="res://sprites/sheet.png" id="1_oe62d"]
+
+[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_rc83b"]
+texture = ExtResource("1_oe62d")
+texture_region_size = Vector2i(32, 32)
+0:3/0 = 0
+0:3/0/custom_data_0 = true
+0:2/0 = 0
+0:2/0/custom_data_1 = true
+1:2/0 = 0
+1:3/0 = 0
+1:3/0/custom_data_0 = true
+2:3/0 = 0
+2:3/0/custom_data_0 = true
+
+[resource]
+tile_size = Vector2i(32, 32)
+custom_data_layer_0/name = "has_collision"
+custom_data_layer_0/type = 1
+custom_data_layer_1/name = "is_berry"
+custom_data_layer_1/type = 1
+sources/1 = SubResource("TileSetAtlasSource_rc83b")
diff --git a/godot/velholib.gdextension b/godot/velholib.gdextension
new file mode 100644
index 0000000..d4ecd23
--- /dev/null
+++ b/godot/velholib.gdextension
@@ -0,0 +1,14 @@
+[configuration]
+entry_symbol = "gdext_rust_init"
+compatibility_minimum = 4.1
+reloadable = true
+
+[libraries]
+linux.debug.x86_64 = "res://../rust/target/debug/libvelho.so"
+linux.release.x86_64 = "res://../rust/target/release/libvelho.so"
+windows.debug.x86_64 = "res://../rust/target/debug/velho.dll"
+windows.release.x86_64 = "res://../rust/target/release/velho.dll"
+macos.debug = "res://../rust/target/debug/libvelho.dylib"
+macos.release = "res://../rust/target/release/libvelho.dylib"
+macos.debug.arm64 = "res://../rust/target/debug/libvelho.dylib"
+macos.release.arm64 = "res://../rust/target/release/libvelho.dylib"
diff --git a/godot/velholib.gdextension.uid b/godot/velholib.gdextension.uid
new file mode 100644
index 0000000..d74195d
--- /dev/null
+++ b/godot/velholib.gdextension.uid
@@ -0,0 +1 @@
+uid://ckdiot1it37u5
diff --git a/rust/.gitignore b/rust/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/rust/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000..895516a
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,203 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "gdextension-api"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ec0a03c8f9c91e3d8eb7ca56dea81c7248c03826dd3f545f33cd22ef275d4d1"
+
+[[package]]
+name = "glam"
+version = "0.30.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2d1aab06663bdce00d6ca5e5ed586ec8d18033a771906c993a1e3755b368d85"
+
+[[package]]
+name = "godot"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab462a365a4b8bcfcdaf93afe767a189a183edfb1d1e697bde9fb26c3f9bfbe9"
+dependencies = [
+ "godot-core",
+ "godot-macros",
+]
+
+[[package]]
+name = "godot-bindings"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597469df0a890b2c7f102b094942e6b184e7ee573adf6ed35fad0421ee86b8fa"
+dependencies = [
+ "gdextension-api",
+]
+
+[[package]]
+name = "godot-cell"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e48bf49cf5d6ef0b64b2a58be980e4854f57e75b822889d639f96c3c098a5ec"
+
+[[package]]
+name = "godot-codegen"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282e80108e93950e66ed3a40f7dcb6f61978ecb0dd2d19ad7028799d6a5537cb"
+dependencies = [
+ "godot-bindings",
+ "heck",
+ "nanoserde",
+ "proc-macro2",
+ "quote",
+ "regex",
+]
+
+[[package]]
+name = "godot-core"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2593ce360312bdec4c45994402dc624c43c3dd8741c8e6391958626dcf635b40"
+dependencies = [
+ "glam",
+ "godot-bindings",
+ "godot-cell",
+ "godot-codegen",
+ "godot-ffi",
+]
+
+[[package]]
+name = "godot-ffi"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce5ae6a7ae29a753f2f0af3d2a2f3194fa482d9900fc167f35a0738fe7d54d2"
+dependencies = [
+ "godot-bindings",
+ "godot-codegen",
+ "godot-macros",
+ "libc",
+]
+
+[[package]]
+name = "godot-macros"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0f3ebf0fe09733267c4631ae0af2d6b1008b322d032832ddcfb4dfc54607ad"
+dependencies = [
+ "godot-bindings",
+ "proc-macro2",
+ "quote",
+ "venial",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "nanoserde"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a36fb3a748a4c9736ed7aeb5f2dfc99665247f1ce306abbddb2bf0ba2ac530a4"
+dependencies = [
+ "nanoserde-derive",
+]
+
+[[package]]
+name = "nanoserde-derive"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a846cbc04412cf509efcd8f3694b114fc700a035fb5a37f21517f9fb019f1ebc"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "velho"
+version = "0.1.0"
+dependencies = [
+ "godot",
+]
+
+[[package]]
+name = "venial"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a42528baceab6c7784446df2a10f4185078c39bf73dc614f154353f1a6b1229"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..254145c
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "velho"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+godot = "0.3.5"
diff --git a/rust/src/common.rs b/rust/src/common.rs
new file mode 100644
index 0000000..be314c1
--- /dev/null
+++ b/rust/src/common.rs
@@ -0,0 +1,136 @@
+use godot::{classes::*, prelude::*};
+
+#[derive(GodotConvert, Var, Export, Clone, Copy, Default, Debug, PartialEq, Eq)]
+#[godot(via = GString)]
+pub enum TurnActorState {
+ /// The default value in Godot.
+ /// Turn manager should call `start_turn` to advance.
+ #[default]
+ WaitingForTurn,
+ /// Currently just automatically and immediately advances to `DecidingAction` state.
+ /// This is reserved for animations and such in the future.
+ TurnStarted,
+ /// An action decision script should call `take_control` to advance from this state.
+ DecidingAction,
+ /// An action script should call `end_turn` to advance from this state.
+ PerformingAction,
+}
+
+/// Turn breakdown:
+///
+/// Actor can be in 4 different states:
+/// 1. Waiting for the turn to start. `TurnActor`s start in this state. (`turn_ended` emitted)
+/// 2. Turn has started, waiting for enabled controls (`turn_started` emitted)
+/// 3. Deciding what to do (`deciding_action` emitted)
+/// 4. Performing the action (`performing_action` emitted)
+/// ```
+#[derive(Debug, GodotClass)]
+#[class(init, base=Node2D)]
+pub struct TurnActor {
+ state: TurnActorState,
+ current_action: Option,
+ 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
+ }
+}