diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..59a150c8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+# CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+dotnet_diagnostic.CS8632.severity = silent
diff --git a/GameJamDungeon.csproj b/GameJamDungeon.csproj
index dbd8b774..1ed1fe8c 100644
--- a/GameJamDungeon.csproj
+++ b/GameJamDungeon.csproj
@@ -20,4 +20,7 @@
+
+
+
\ No newline at end of file
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
new file mode 100644
index 00000000..6e921338
--- /dev/null
+++ b/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "Godot": {
+ "commandName": "Executable",
+ "executablePath": "%GODOT%",
+ "commandLineArgs": "--path . --verbose",
+ "workingDirectory": ".",
+ "nativeDebugging": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..df432610
--- /dev/null
+++ b/global.json
@@ -0,0 +1,8 @@
+{
+ "sdk": {
+ "version": "8.0.400"
+ },
+ "msbuild-sdks": {
+ "Godot.NET.Sdk": "4.3.0"
+ }
+}
\ No newline at end of file
diff --git a/project.godot b/project.godot
index 4b5ecf46..942a88f1 100644
--- a/project.godot
+++ b/project.godot
@@ -69,43 +69,7 @@ Inventory={
"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":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
]
}
-move_forward={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"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":0,"location":0,"echo":false,"script":null)
-]
-}
-move_backward={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"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":0,"location":0,"echo":false,"script":null)
-]
-}
-move_right={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"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":0,"location":0,"echo":false,"script":null)
-]
-}
-move_left={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"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":0,"location":0,"echo":false,"script":null)
-]
-}
-move_sprint={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
-move_jump={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"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":0,"location":0,"echo":false,"script":null)
-]
-}
-move_crouch={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
-move_fly_mode={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
+
+[physics]
+
+common/physics_ticks_per_second=144
diff --git a/src/app/App.cs b/src/app/App.cs
new file mode 100644
index 00000000..769cf366
--- /dev/null
+++ b/src/app/App.cs
@@ -0,0 +1,61 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using Godot;
+
+namespace GameJamDungeon
+{
+ public interface IApp : ICanvasLayer, IProvide;
+
+ [Meta(typeof(IAutoNode))]
+ public partial class App : CanvasLayer, IApp
+ {
+ public override void _Notification(int what) => this.Notify(what);
+
+ public const string GAME_SCENE_PATH = "res://src/game/Game.tscn";
+
+ public IGame Game { get; set; } = default!;
+
+ public IInstantiator Instantiator { get; set; } = default!;
+
+ IAppRepo IProvide.Value() => AppRepo;
+
+ public IAppRepo AppRepo { get; set; } = default!;
+ public IAppLogic AppLogic { get; set; } = default!;
+ public AppLogic.IBinding AppBinding { get; set; } = default!;
+
+ [Node] public ISubViewport GameWindow { get; set; } = default!;
+
+ public void Initialize()
+ {
+ Instantiator = new Instantiator(GetTree());
+ AppRepo = new AppRepo();
+ AppLogic = new AppLogic();
+ AppLogic.Set(AppRepo);
+
+ this.Provide();
+ }
+
+ public void OnReady()
+ {
+ Game = Instantiator.LoadAndInstantiate(GAME_SCENE_PATH);
+ GameWindow.AddChildEx(Game);
+ AppBinding = AppLogic.Bind();
+ AppLogic.Start();
+
+ Instantiator.SceneTree.Paused = false;
+ }
+
+
+ public void OnNewGame() => AppLogic.Input(new AppLogic.Input.NewGame());
+
+ public void OnQuit() => AppLogic.Input(new AppLogic.Input.QuitGame());
+
+ public void OnExitTree()
+ {
+ AppLogic.Stop();
+ AppBinding.Dispose();
+ AppRepo.Dispose();
+ }
+ }
+}
diff --git a/src/app/App.tscn b/src/app/App.tscn
index e23b2b58..56e215d2 100644
--- a/src/app/App.tscn
+++ b/src/app/App.tscn
@@ -1,89 +1,22 @@
-[gd_scene load_steps=8 format=3 uid="uid://cagfc5ridmteu"]
+[gd_scene load_steps=2 format=3 uid="uid://cagfc5ridmteu"]
-[ext_resource type="PackedScene" uid="uid://wg25dg65ksgg" path="res://src/map/dungeon/DungeonGenerator.tscn" id="1_eapuk"]
-[ext_resource type="PackedScene" uid="uid://dhpwwqow1ahrc" path="res://src/map/dungeon/rooms/Room1.tscn" id="2_x112h"]
-[ext_resource type="Script" path="res://addons/SimpleDungeons/DungeonRoom3D.gd" id="3_614ds"]
-[ext_resource type="PackedScene" uid="uid://bn4gslp2gk8ds" path="res://src/map/dungeon/corridor/Corridor.tscn" id="4_tex41"]
-[ext_resource type="Script" path="res://src/character_controller_3d.gd" id="4_vtqs3"]
+[ext_resource type="Script" path="res://src/app/App.cs" id="1_rt73h"]
-[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_ult2r"]
-radius = 0.25
-height = 0.5
+[node name="App" type="CanvasLayer"]
+script = ExtResource("1_rt73h")
-[sub_resource type="Environment" id="Environment_3njg4"]
+[node name="SubViewportContainer" type="SubViewportContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+stretch = true
-[node name="App" type="Node"]
-
-[node name="DungeonGenerator3D" parent="." instance=ExtResource("1_eapuk")]
-
-[node name="RoomsContainer" type="Node3D" parent="DungeonGenerator3D"]
-
-[node name="DungeonRoom3D_0" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("2_x112h")]
-transform = Transform3D(1.19249e-08, 0, -1, 0, 1, 0, 1, 0, 1.19249e-08, 35, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="DungeonRoom3D_1" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("2_x112h")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0, 35)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_2" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 25, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_3" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_4" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_5" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_6" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_7" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_8" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 0, 45)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_9" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0, 25)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_10" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 25)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_11" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 25)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_12" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 25)
-script = ExtResource("3_614ds")
-
-[node name="Corridor_13" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_tex41")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 35)
-script = ExtResource("3_614ds")
-
-[node name="CharacterBody3D" type="CharacterBody3D" parent="."]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14.0223, -3.8587, -16.1332)
-script = ExtResource("4_vtqs3")
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="CharacterBody3D"]
-shape = SubResource("CapsuleShape3D_ult2r")
-
-[node name="OmniLight3D" type="OmniLight3D" parent="CharacterBody3D"]
-
-[node name="Camera3D" type="Camera3D" parent="CharacterBody3D"]
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
-environment = SubResource("Environment_3njg4")
+[node name="GameWindow" type="SubViewport" parent="SubViewportContainer"]
+unique_name_in_owner = true
+transparent_bg = true
+handle_input_locally = false
+audio_listener_enable_3d = true
+size = Vector2i(1920, 1080)
+render_target_update_mode = 4
diff --git a/src/app/domain/AppRepo.cs b/src/app/domain/AppRepo.cs
new file mode 100644
index 00000000..5bddc463
--- /dev/null
+++ b/src/app/domain/AppRepo.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace GameJamDungeon
+{
+ public interface IAppRepo : IDisposable
+ {
+ event Action? GameEntered;
+
+ event Action? GameExited;
+
+ event Action? SplashScreenSkipped;
+
+ event Action? MainMenuEntered;
+
+ void SkipSplashScreen();
+
+ void OnMainMenuEntered();
+
+ void OnEnterGame();
+
+ void OnExitGame();
+
+ void OnGameOver();
+ }
+
+ public class AppRepo : IAppRepo
+ {
+ public event Action? SplashScreenSkipped;
+ public event Action? MainMenuEntered;
+ public event Action? GameEntered;
+ public event Action? GameExited;
+
+ private bool _disposedValue;
+
+ public void SkipSplashScreen() => SplashScreenSkipped?.Invoke();
+
+ public void OnMainMenuEntered() => MainMenuEntered?.Invoke();
+
+ public void OnEnterGame() => GameEntered?.Invoke();
+
+ public void OnExitGame() => GameExited?.Invoke();
+
+ public void OnGameOver() => GameExited?.Invoke();
+
+ protected void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ // Dispose managed objects.
+ SplashScreenSkipped = null;
+ MainMenuEntered = null;
+ GameEntered = null;
+ GameExited = null;
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/app/state/AppLogic.Input.cs b/src/app/state/AppLogic.Input.cs
new file mode 100644
index 00000000..5d9227a5
--- /dev/null
+++ b/src/app/state/AppLogic.Input.cs
@@ -0,0 +1,18 @@
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public static class Input
+ {
+ public readonly record struct NewGame;
+
+ public readonly record struct FadeInFinished;
+
+ public readonly record struct FadeOutFinished;
+
+ public readonly record struct QuitGame;
+
+ public readonly record struct GameOver;
+ }
+ }
+}
diff --git a/src/app/state/AppLogic.Output.cs b/src/app/state/AppLogic.Output.cs
new file mode 100644
index 00000000..67652042
--- /dev/null
+++ b/src/app/state/AppLogic.Output.cs
@@ -0,0 +1,30 @@
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public static class Output
+ {
+ public readonly record struct FadeToBlack;
+
+ public readonly record struct ShowSplashScreen;
+
+ public readonly record struct HideSplashScreen;
+
+ public readonly record struct RemoveExistingGame;
+
+ public readonly record struct PlayGame;
+
+ public readonly record struct ShowGame;
+
+ public readonly record struct HideGame;
+
+ public readonly record struct SetupGameScene();
+
+ public readonly record struct ShowMainMenu;
+
+ public readonly record struct ExitGame;
+
+ public readonly record struct GameOver;
+ }
+ }
+}
diff --git a/src/app/state/AppLogic.State.cs b/src/app/state/AppLogic.State.cs
new file mode 100644
index 00000000..df66a45f
--- /dev/null
+++ b/src/app/state/AppLogic.State.cs
@@ -0,0 +1,11 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ [Meta]
+ public abstract partial record State : StateLogic;
+ }
+}
diff --git a/src/app/state/AppLogic.cs b/src/app/state/AppLogic.cs
new file mode 100644
index 00000000..bf9fbaee
--- /dev/null
+++ b/src/app/state/AppLogic.cs
@@ -0,0 +1,14 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public interface IAppLogic : ILogicBlock;
+
+ [Meta]
+ [LogicBlock(typeof(State), Diagram = true)]
+ public partial class AppLogic : LogicBlock, IAppLogic
+ {
+ public override Transition GetInitialState() => To();
+ }
+}
diff --git a/src/app/state/AppLogic.g.puml b/src/app/state/AppLogic.g.puml
new file mode 100644
index 00000000..73eb587c
--- /dev/null
+++ b/src/app/state/AppLogic.g.puml
@@ -0,0 +1,25 @@
+@startuml AppLogic
+state "AppLogic State" as GameJam2024Practice_AppLogic_State {
+ state "InGame" as GameJam2024Practice_AppLogic_State_InGame
+ state "MainMenu" as GameJam2024Practice_AppLogic_State_MainMenu
+ state "SplashScreen" as GameJam2024Practice_AppLogic_State_SplashScreen
+ state "LeavingMenu" as GameJam2024Practice_AppLogic_State_LeavingMenu
+}
+
+GameJam2024Practice_AppLogic_State_InGame --> GameJam2024Practice_AppLogic_State_MainMenu : GameOver
+GameJam2024Practice_AppLogic_State_LeavingMenu --> GameJam2024Practice_AppLogic_State_InGame : FadeOutFinished
+GameJam2024Practice_AppLogic_State_MainMenu --> GameJam2024Practice_AppLogic_State_LeavingMenu : NewGame
+GameJam2024Practice_AppLogic_State_MainMenu --> GameJam2024Practice_AppLogic_State_MainMenu : QuitGame
+GameJam2024Practice_AppLogic_State_SplashScreen --> GameJam2024Practice_AppLogic_State_MainMenu : FadeOutFinished
+
+GameJam2024Practice_AppLogic_State_InGame : OnEnter → ShowGame
+GameJam2024Practice_AppLogic_State_InGame : OnExit → HideGame
+GameJam2024Practice_AppLogic_State_InGame : OnGameOver → RemoveExistingGame
+GameJam2024Practice_AppLogic_State_LeavingMenu : OnEnter → FadeToBlack
+GameJam2024Practice_AppLogic_State_MainMenu : OnEnter → SetupGameScene, ShowMainMenu
+GameJam2024Practice_AppLogic_State_MainMenu : OnQuitGame → ExitGame
+GameJam2024Practice_AppLogic_State_SplashScreen : OnEnter → ShowSplashScreen
+GameJam2024Practice_AppLogic_State_SplashScreen : OnSplashScreenSkipped() → HideSplashScreen
+
+[*] --> GameJam2024Practice_AppLogic_State_SplashScreen
+@enduml
\ No newline at end of file
diff --git a/src/app/state/states/InGame.cs b/src/app/state/states/InGame.cs
new file mode 100644
index 00000000..320b4976
--- /dev/null
+++ b/src/app/state/states/InGame.cs
@@ -0,0 +1,37 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record InGame : State, IGet
+ {
+ public InGame()
+ {
+
+ this.OnEnter(() =>
+ {
+ Get().OnEnterGame();
+ Output(new Output.ShowGame());
+ });
+ this.OnExit(() => Output(new Output.HideGame()));
+
+ OnAttach(() => Get().GameExited += OnGameExited);
+ OnDetach(() => Get().GameExited -= OnGameExited);
+ }
+
+ public Transition On(in Input.GameOver input)
+ {
+ Output(new Output.RemoveExistingGame());
+ return To();
+ }
+
+ public void OnGameExited() => Input(new Input.GameOver());
+ }
+ }
+ }
+}
diff --git a/src/app/state/states/LeavingMenu.cs b/src/app/state/states/LeavingMenu.cs
new file mode 100644
index 00000000..e27562a1
--- /dev/null
+++ b/src/app/state/states/LeavingMenu.cs
@@ -0,0 +1,22 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record LeavingMenu : State, IGet
+ {
+ public LeavingMenu()
+ {
+ this.OnEnter(() => Output(new Output.FadeToBlack()));
+ }
+
+ public Transition On(in Input.FadeOutFinished input) => To();
+ }
+ }
+ }
+}
diff --git a/src/app/state/states/MainMenu.cs b/src/app/state/states/MainMenu.cs
new file mode 100644
index 00000000..fc9e9bc5
--- /dev/null
+++ b/src/app/state/states/MainMenu.cs
@@ -0,0 +1,35 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record MainMenu : State, IGet, IGet
+ {
+ public MainMenu()
+ {
+ this.OnEnter(() =>
+ {
+ Output(new Output.SetupGameScene());
+
+ Get().OnMainMenuEntered();
+
+ Output(new Output.ShowMainMenu());
+ });
+ }
+ public Transition On(in Input.NewGame input) => To();
+
+ public Transition On(in Input.QuitGame input)
+ {
+ Output(new Output.ExitGame());
+
+ return ToSelf();
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/state/states/SplashScreen.cs b/src/app/state/states/SplashScreen.cs
new file mode 100644
index 00000000..e00b4f8c
--- /dev/null
+++ b/src/app/state/states/SplashScreen.cs
@@ -0,0 +1,33 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class AppLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record SplashScreen : State, IGet
+ {
+ public SplashScreen()
+ {
+ this.OnEnter(() => Output(new Output.ShowSplashScreen()));
+
+ OnAttach(
+ () => Get().SplashScreenSkipped += OnSplashScreenSkipped
+ );
+
+ OnDetach(
+ () => Get().SplashScreenSkipped -= OnSplashScreenSkipped
+ );
+ }
+
+ public Transition On(in Input.FadeOutFinished input) => To();
+
+ public void OnSplashScreenSkipped() =>
+ Output(new Output.HideSplashScreen());
+ }
+ }
+ }
+}
diff --git a/src/character_controller_3d.gd b/src/character_controller_3d.gd
deleted file mode 100644
index 854771d2..00000000
--- a/src/character_controller_3d.gd
+++ /dev/null
@@ -1,53 +0,0 @@
-extends CharacterBody3D
-
-@export var FORWARD_SPEED = 2.0
-@export var BACK_SPEED = 1.0
-@export var TURN_SPEED = 0.025
-
-var Vec3Z = Vector3.ZERO
-
-#OPTIONAL: These could be used to change sensitivity of either rotating z or y
-#var M_LOOK_SENS = 1
-#var V_LOOK_SENS = 1
-
-func _physics_process(delta: float) -> void:
- if Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_backward"):
- velocity.x = 0
- velocity.z = 0
-
- elif Input.is_action_pressed("move_forward"):
- var forwardVector = -Vector3.FORWARD.rotated(Vector3.UP, rotation.y)
- velocity = -forwardVector * FORWARD_SPEED
-
- elif Input.is_action_pressed("move_backward"):
- var backwardVector = Vector3.FORWARD.rotated(Vector3.UP, rotation.y)
- velocity = -backwardVector * BACK_SPEED
-
- #If pressing nothing stop velocity
- else:
- velocity.x = 0
- velocity.z = 0
-
- # IF turn left WHILE moving back, turn right
- if Input.is_action_pressed("move_left") and Input.is_action_pressed("move_backward"):
- rotation.z -= Vec3Z.y + TURN_SPEED #* V_LOOK_SENS
- rotation.z = clamp(rotation.x, -50, 90)
- rotation.y -= Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
-
- elif Input.is_action_pressed("move_left"):
- rotation.z += Vec3Z.y - TURN_SPEED #* V_LOOK_SENS
- rotation.z = clamp(rotation.x, -50, 90)
- rotation.y += Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
-
- # IF turn right WHILE moving back, turn left
- if Input.is_action_pressed("move_right") and Input.is_action_pressed("move_backward"):
- rotation.z += Vec3Z.y - TURN_SPEED #* V_LOOK_SENS
- rotation.z = clamp(rotation.x, -50, 90)
- rotation.y += Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
-
- elif Input.is_action_pressed("move_right"):
- rotation.z -= Vec3Z.y + TURN_SPEED #* V_LOOK_SENS
- rotation.z = clamp(rotation.x, -50, 90)
- rotation.y -= Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
-
- move_and_slide()
diff --git a/src/game/Game.cs b/src/game/Game.cs
new file mode 100644
index 00000000..6fe0d4cd
--- /dev/null
+++ b/src/game/Game.cs
@@ -0,0 +1,77 @@
+namespace GameJamDungeon;
+
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using Godot;
+
+public interface IGame : IProvide, INode3D
+{
+}
+
+[Meta(typeof(IAutoNode))]
+public partial class Game : Node3D, IGame
+{
+ public override void _Notification(int what) => this.Notify(what);
+
+ IGameRepo IProvide.Value() => GameRepo;
+
+ public IGameLogic GameLogic { get; set; } = default!;
+
+ public IGameRepo GameRepo { get; set; } = default!;
+
+ public GameLogic.IBinding GameBinding { get; set; } = default!;
+
+ [Dependency] public IAppRepo AppRepo => this.DependOn();
+
+ public void Setup()
+ {
+ GameRepo = new GameRepo();
+ GameLogic = new GameLogic();
+ GameLogic.Set(GameRepo);
+ GameLogic.Set(AppRepo);
+ }
+
+ public void OnResolved()
+ {
+ GameBinding = GameLogic.Bind();
+ GameBinding
+ .Handle((in GameLogic.Output.StartGame _) => { GameRepo.Resume(); })
+ .Handle((in GameLogic.Output.SetPauseMode output) => { CallDeferred(nameof(SetPauseMode), output.IsPaused); })
+ .Handle((in GameLogic.Output.GameOver _) => { AppRepo.OnGameOver(); });
+
+ GameLogic.Input(new GameLogic.Input.Initialize());
+
+ this.Provide();
+ GameLogic.Start();
+ }
+
+ public override void _Input(InputEvent @event)
+ {
+ if (Input.IsActionJustPressed(GameInputs.Inventory))
+ {
+ GD.Print("Inventory button pressed");
+ GameLogic.Input(new GameLogic.Input.InventoryMenuButtonPressed());
+ }
+
+ if (Input.IsActionJustPressed(GameInputs.MiniMap))
+ {
+ GD.Print("MiniMap button pressed");
+ GameLogic.Input(new GameLogic.Input.MiniMapButtonPressed());
+ }
+
+ if (Input.IsActionJustReleased(GameInputs.MiniMap))
+ {
+ GD.Print("MiniMap button released");
+ GameLogic.Input(new GameLogic.Input.MiniMapButtonReleased());
+ }
+ }
+
+ private void SetPauseMode(bool isPaused)
+ {
+ if (GetTree() != null)
+ GetTree().Paused = isPaused;
+ }
+
+ public void OnStart() => GameLogic.Input(new GameLogic.Input.Start());
+}
diff --git a/src/game/Game.tscn b/src/game/Game.tscn
new file mode 100644
index 00000000..e9b28e0d
--- /dev/null
+++ b/src/game/Game.tscn
@@ -0,0 +1,83 @@
+[gd_scene load_steps=8 format=3 uid="uid://33ek675mfb5n"]
+
+[ext_resource type="Script" path="res://src/game/Game.cs" id="1_ytcii"]
+[ext_resource type="PackedScene" uid="uid://wg25dg65ksgg" path="res://src/map/dungeon/DungeonGenerator.tscn" id="2_cgboj"]
+[ext_resource type="PackedScene" uid="uid://cfecvvav8kkp6" path="res://src/player/Player.tscn" id="3_kk6ly"]
+[ext_resource type="PackedScene" uid="uid://dhpwwqow1ahrc" path="res://src/map/dungeon/rooms/Room1.tscn" id="4_56rmd"]
+[ext_resource type="Script" path="res://addons/SimpleDungeons/DungeonRoom3D.gd" id="5_5i8m5"]
+[ext_resource type="PackedScene" uid="uid://bn4gslp2gk8ds" path="res://src/map/dungeon/corridor/Corridor.tscn" id="6_5fcqc"]
+
+[sub_resource type="Environment" id="Environment_fke5g"]
+
+[node name="Game" type="Node3D"]
+script = ExtResource("1_ytcii")
+
+[node name="DungeonGenerator3D" parent="." instance=ExtResource("2_cgboj")]
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="DungeonGenerator3D"]
+environment = SubResource("Environment_fke5g")
+
+[node name="Player" parent="DungeonGenerator3D" instance=ExtResource("3_kk6ly")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.55927, -4.70883, 25.1515)
+
+[node name="RoomsContainer" type="Node3D" parent="DungeonGenerator3D"]
+
+[node name="DungeonRoom3D_0" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_56rmd")]
+transform = Transform3D(1.19249e-08, 0, -1, 0, 1, 0, 1, 0, 1.19249e-08, 5, 0, 15)
+script = ExtResource("5_5i8m5")
+
+[node name="DungeonRoom3D_1" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("4_56rmd")]
+transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, -25, 0, -25)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_2" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_3" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 5)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_4" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, -5)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_5" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, -15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_6" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_7" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -25)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_8" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_9" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 5)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_10" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_11" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -35, 0, -25)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_12" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -35, 0, -15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_13" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0, -15)
+script = ExtResource("5_5i8m5")
+
+[node name="Corridor_14" type="Node3D" parent="DungeonGenerator3D/RoomsContainer" instance=ExtResource("6_5fcqc")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -5)
+script = ExtResource("5_5i8m5")
diff --git a/src/game/GameData.cs b/src/game/GameData.cs
new file mode 100644
index 00000000..8bea4b65
--- /dev/null
+++ b/src/game/GameData.cs
@@ -0,0 +1,10 @@
+using Chickensoft.Introspection;
+
+namespace GameJamDungeon
+{
+ [Meta, Id("game_data")]
+ public partial record GameData
+ {
+ public required PlayerData PlayerData { get; init; }
+ }
+}
diff --git a/src/game/GameLogic.Input.cs b/src/game/GameLogic.Input.cs
new file mode 100644
index 00000000..9b248579
--- /dev/null
+++ b/src/game/GameLogic.Input.cs
@@ -0,0 +1,20 @@
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public static class Input
+ {
+ public readonly record struct Start;
+
+ public readonly record struct Initialize;
+
+ public readonly record struct InventoryMenuButtonPressed;
+
+ public readonly record struct MiniMapButtonPressed;
+
+ public readonly record struct MiniMapButtonReleased;
+
+ public readonly record struct GameOver;
+ }
+ }
+}
diff --git a/src/game/GameLogic.Output.cs b/src/game/GameLogic.Output.cs
new file mode 100644
index 00000000..4d7af89a
--- /dev/null
+++ b/src/game/GameLogic.Output.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public static class Output
+ {
+ public readonly record struct StartGame();
+
+ public readonly record struct SetInventoryMode(List Inventory);
+
+ public readonly record struct HideInventory();
+
+ public readonly record struct SetPauseMode(bool IsPaused);
+
+ public readonly record struct ShowMiniMap();
+
+ public readonly record struct HideMiniMap();
+
+ public readonly record struct GameOver();
+ }
+ }
+}
diff --git a/src/game/GameLogic.State.cs b/src/game/GameLogic.State.cs
new file mode 100644
index 00000000..20e7acf2
--- /dev/null
+++ b/src/game/GameLogic.State.cs
@@ -0,0 +1,28 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ [Meta]
+ public abstract partial record State : StateLogic
+ {
+ protected State()
+ {
+ OnAttach(() =>
+ {
+ var gameRepo = Get();
+ gameRepo.IsPaused.Sync += OnIsPaused;
+ });
+ OnDetach(() =>
+ {
+ var gameRepo = Get();
+ gameRepo.IsPaused.Sync -= OnIsPaused;
+ });
+ }
+
+ public void OnIsPaused(bool isPaused) => Output(new Output.SetPauseMode(isPaused));
+ }
+ }
+}
diff --git a/src/game/GameLogic.cs b/src/game/GameLogic.cs
new file mode 100644
index 00000000..d8d7a6b1
--- /dev/null
+++ b/src/game/GameLogic.cs
@@ -0,0 +1,14 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public interface IGameLogic : ILogicBlock;
+
+ [Meta]
+ [LogicBlock(typeof(State), Diagram = true)]
+ public partial class GameLogic : LogicBlock, IGameLogic
+ {
+ public override Transition GetInitialState() => To();
+ }
+}
diff --git a/src/game/IGameRepo.cs b/src/game/IGameRepo.cs
new file mode 100644
index 00000000..7105cb87
--- /dev/null
+++ b/src/game/IGameRepo.cs
@@ -0,0 +1,98 @@
+using Chickensoft.Collections;
+using Godot;
+using System;
+using System.Collections.Generic;
+
+namespace GameJamDungeon
+{
+ public interface IGameRepo : IDisposable
+ {
+ event Action? Ended;
+
+ IAutoProp> InventoryItems { get; }
+
+ IAutoProp IsInventoryScreenOpened { get; }
+
+ IAutoProp IsPaused { get; }
+
+ void Pause();
+
+ void Resume();
+
+ IAutoProp PlayerGlobalPosition { get; }
+
+ void SetPlayerGlobalPosition(Vector3 playerGlobalPosition);
+ }
+
+ public class GameRepo : IGameRepo
+ {
+ public event Action? Ended;
+
+ private readonly AutoProp> _inventoryItems;
+ private readonly AutoProp _isInventoryScreenOpened;
+
+ public IAutoProp> InventoryItems => _inventoryItems;
+
+ public IAutoProp IsInventoryScreenOpened => _isInventoryScreenOpened;
+
+ public IAutoProp PlayerGlobalPosition => _playerGlobalPosition;
+ private readonly AutoProp _playerGlobalPosition;
+
+ public IAutoProp IsPaused => _isPaused;
+ private readonly AutoProp _isPaused;
+
+ private bool _disposedValue;
+
+ public GameRepo()
+ {
+ _inventoryItems = new AutoProp>([]);
+ _isInventoryScreenOpened = new AutoProp(false);
+ _isPaused = new AutoProp(false);
+ _playerGlobalPosition = new AutoProp(Vector3.Zero);
+ }
+
+ public void Pause()
+ {
+ _isPaused.OnNext(true);
+ GD.Print("Paused");
+ }
+
+ public void Resume()
+ {
+ _isPaused.OnNext(false);
+ GD.Print("Resume");
+ }
+
+ public void SetPlayerGlobalPosition(Vector3 playerGlobalPosition) => _playerGlobalPosition.OnNext(playerGlobalPosition);
+
+ public void OnGameEnded()
+ {
+ Pause();
+ Ended?.Invoke();
+ }
+
+ protected void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _playerGlobalPosition.OnCompleted();
+ _playerGlobalPosition.Dispose();
+ _inventoryItems.OnCompleted();
+ _inventoryItems.Dispose();
+ _isInventoryScreenOpened.OnCompleted();
+ _isInventoryScreenOpened.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/game/state/states/GameLogic.State.Quit.cs b/src/game/state/states/GameLogic.State.Quit.cs
new file mode 100644
index 00000000..098b8339
--- /dev/null
+++ b/src/game/state/states/GameLogic.State.Quit.cs
@@ -0,0 +1,20 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record Quit : State
+ {
+ public Quit()
+ {
+ this.OnEnter(() => Output(new Output.GameOver()));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/game/state/states/InventoryOpened.cs b/src/game/state/states/InventoryOpened.cs
new file mode 100644
index 00000000..a6c9ac27
--- /dev/null
+++ b/src/game/state/states/InventoryOpened.cs
@@ -0,0 +1,22 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record InventoryOpened : State, IGet
+ {
+ public InventoryOpened()
+ {
+ this.OnEnter(() => { Get().Pause(); Output(new Output.SetInventoryMode(Get().InventoryItems.Value)); });
+ this.OnExit(() => { Output(new Output.HideInventory()); });
+ }
+ public Transition On(in Input.InventoryMenuButtonPressed input) => To();
+ }
+ }
+ }
+}
diff --git a/src/game/state/states/MenuBackdrop.cs b/src/game/state/states/MenuBackdrop.cs
new file mode 100644
index 00000000..3f840ce3
--- /dev/null
+++ b/src/game/state/states/MenuBackdrop.cs
@@ -0,0 +1,29 @@
+using Chickensoft.Introspection;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record MenuBackdrop : State, IGet, IGet
+ {
+ public MenuBackdrop()
+ {
+ OnAttach(() => Get().GameEntered += OnGameEntered);
+ OnDetach(() => Get().GameEntered -= OnGameEntered);
+ }
+
+ public void OnGameEntered() => Input(new Input.Start());
+
+ public Transition On(in Input.Start input) => To();
+
+ public Transition On(in Input.Initialize input)
+ {
+ return ToSelf();
+ }
+ }
+ }
+ }
+}
diff --git a/src/game/state/states/MiniMapOpen.cs b/src/game/state/states/MiniMapOpen.cs
new file mode 100644
index 00000000..8bb95971
--- /dev/null
+++ b/src/game/state/states/MiniMapOpen.cs
@@ -0,0 +1,23 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record MinimapOpen : State, IGet
+ {
+ public MinimapOpen()
+ {
+ this.OnEnter(() => { Get().Pause(); Output(new Output.ShowMiniMap()); });
+ this.OnExit(() => { Output(new Output.HideMiniMap()); });
+ }
+
+ public Transition On(in Input.MiniMapButtonReleased input) => To();
+ }
+ }
+ }
+}
diff --git a/src/game/state/states/Paused.cs b/src/game/state/states/Paused.cs
new file mode 100644
index 00000000..989bf669
--- /dev/null
+++ b/src/game/state/states/Paused.cs
@@ -0,0 +1,26 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record Paused : State, IGet, IGet
+ {
+ public Paused()
+ {
+ this.OnEnter(() => Get().Pause());
+ this.OnExit(() => Output(new Output.SetPauseMode(false)));
+ }
+
+
+ public virtual Transition On(in Input.InventoryMenuButtonPressed input) => To();
+
+ public virtual Transition On(in Input.MiniMapButtonReleased input) => To();
+ }
+ }
+ }
+}
diff --git a/src/game/state/states/Playing.cs b/src/game/state/states/Playing.cs
new file mode 100644
index 00000000..741e0481
--- /dev/null
+++ b/src/game/state/states/Playing.cs
@@ -0,0 +1,34 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class GameLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record Playing : State, IGet, IGet, IGet
+ {
+ public Playing()
+ {
+ this.OnEnter(() => { Output(new Output.StartGame()); Get().Resume(); });
+
+ OnAttach(() => Get().Ended += OnEnded);
+ OnDetach(() => Get().Ended -= OnEnded);
+ }
+
+ public void OnEnded() => Input(new Input.GameOver());
+
+ public Transition On(in Input.InventoryMenuButtonPressed input) => To();
+
+ public Transition On(in Input.MiniMapButtonPressed input) => To();
+
+ public Transition On(in Input.GameOver input)
+ {
+ return To();
+ }
+ }
+ }
+ }
+}
diff --git a/src/inventory_menu/InventoryMenu.cs b/src/inventory_menu/InventoryMenu.cs
new file mode 100644
index 00000000..3eac8bff
--- /dev/null
+++ b/src/inventory_menu/InventoryMenu.cs
@@ -0,0 +1,126 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using GameJamDungeon;
+using Godot;
+using System.Collections.Generic;
+
+public interface IInventoryMenu : IControl
+{
+ public void PopulateItems(List items);
+
+ public void ClearItems();
+}
+
+[Meta(typeof(IAutoNode))]
+public partial class InventoryMenu : Control, IInventoryMenu
+{
+ public override void _Notification(int what) => this.Notify(what);
+
+ [Node] public IVBoxContainer ItemList { get; set; } = default!;
+
+ [Node] public TextureRect Cursor { get; set; } = default!;
+
+ [Dependency] public IGameRepo GameRepo => this.DependOn();
+
+ private int _currentSelection = 0;
+
+ public void PopulateItems(List items)
+ {
+ foreach (var item in items)
+ {
+ var label = new WeaponLabel(item) { Text = item.Name };
+ ItemList.AddChild(label);
+ }
+
+ if (ItemList.GetChildCount() > 0)
+ CallDeferred(nameof(InitializeInventoryMenu));
+ }
+
+ public void InitializeInventoryMenu()
+ {
+ if (ItemList.GetChildCount() > 0)
+ {
+ var currentItem = ItemList.GetChild(_currentSelection);
+ SetCursorLocation(currentItem);
+ }
+ }
+
+ public void ClearItems()
+ {
+ foreach (var item in ItemList.GetChildren())
+ ItemList.RemoveChild(item);
+ }
+
+ public void SetCursorLocation(Control menuItem)
+ {
+ var position = menuItem.GlobalPosition;
+ var size = menuItem.Size;
+
+ Cursor.GlobalPosition = new Vector2(position.X, position.Y + size.Y / 2.0f) - Cursor.Size / 2.0f - new Vector2(15, -5);
+ }
+
+ public void SetCursorToPrevious()
+ {
+ if (ItemList.GetChildCount() == 0)
+ return;
+
+ if (_currentSelection > 0)
+ {
+ _currentSelection -= 1;
+ var selectedMenuItem = ItemList.GetChild(_currentSelection);
+ SetCursorLocation(selectedMenuItem);
+ }
+ }
+
+ public void SetCursorToNext()
+ {
+ if (ItemList.GetChildCount() == 0)
+ return;
+
+ if (_currentSelection < ItemList.GetChildCount() - 1)
+ {
+ _currentSelection += 1;
+ var selectedMenuItem = ItemList.GetChild(_currentSelection);
+ SetCursorLocation(selectedMenuItem);
+ }
+ }
+
+ private void UnequipItem(WeaponLabel item)
+ {
+ item.UnequipItem();
+ }
+
+ public override void _Process(double delta)
+ {
+ var input = Vector2.Zero;
+ if (Input.IsActionJustPressed(GameInputs.MoveUp))
+ SetCursorToPrevious();
+ if (Input.IsActionJustPressed(GameInputs.MoveDown))
+ SetCursorToNext();
+ }
+}
+
+public partial class WeaponLabel : Label
+{
+ public WeaponLabel(InventoryItem inventoryItem)
+ {
+ InventoryItem = inventoryItem;
+ LabelSettings = UnequippedItemFont;
+ }
+
+ public InventoryItem InventoryItem { get; set; } = default!;
+
+ private static LabelSettings UnequippedItemFont => GD.Load("res://src/vfx/Fonts/InventoryLabelSettings.tres");
+ private static LabelSettings EquippedItemFont => GD.Load("res://src/vfx/Fonts/EquippedInventoryLabelSettings.tres");
+
+ public void EquipItem()
+ {
+ LabelSettings = EquippedItemFont;
+ }
+
+ public void UnequipItem()
+ {
+ LabelSettings = UnequippedItemFont;
+ }
+}
diff --git a/src/items/ArmorItem.cs b/src/items/ArmorItem.cs
new file mode 100644
index 00000000..7e837e2f
--- /dev/null
+++ b/src/items/ArmorItem.cs
@@ -0,0 +1,8 @@
+using Godot;
+
+[GlobalClass]
+public partial class ArmorItem : InventoryItem
+{
+ [Export]
+ public required int Defense { get; set; }
+}
diff --git a/src/items/InventoryItem.cs b/src/items/InventoryItem.cs
new file mode 100644
index 00000000..99a7ac20
--- /dev/null
+++ b/src/items/InventoryItem.cs
@@ -0,0 +1,11 @@
+using Godot;
+
+[GlobalClass]
+public partial class InventoryItem : Resource
+{
+ [Export]
+ public string Name = string.Empty;
+
+ [Export]
+ public string Description = string.Empty;
+}
\ No newline at end of file
diff --git a/src/items/WeaponItem.cs b/src/items/WeaponItem.cs
new file mode 100644
index 00000000..877b0a08
--- /dev/null
+++ b/src/items/WeaponItem.cs
@@ -0,0 +1,10 @@
+using Godot;
+
+[GlobalClass]
+public partial class WeaponItem : InventoryItem
+{
+ [Export]
+ public required int Damage { get; set; }
+
+ public static WeaponItem Default => new WeaponItem() { Damage = 1 };
+}
diff --git a/src/map/dungeon/corridor/Corridor.tscn b/src/map/dungeon/corridor/Corridor.tscn
index 91a42a45..928f83ac 100644
--- a/src/map/dungeon/corridor/Corridor.tscn
+++ b/src/map/dungeon/corridor/Corridor.tscn
@@ -8,6 +8,7 @@
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_nsah4"]
transparency = 1
+albedo_texture = ExtResource("2_6scux")
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_18cgv"]
albedo_texture = ExtResource("2_6scux")
@@ -40,19 +41,19 @@ size = Vector3(9, 9, 9)
material = SubResource("StandardMaterial3D_18cgv")
[node name="DOOR?" parent="CSGBox3D" instance=ExtResource("2_vpnlr")]
-transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, -5, -3.5, 0)
+transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, -5, -2.58627, 0)
material = SubResource("StandardMaterial3D_cquyy")
[node name="DOOR?3" parent="CSGBox3D" instance=ExtResource("2_vpnlr")]
-transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, 5, -3.5, 0)
+transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, 5, -2.58627, 0)
material = SubResource("StandardMaterial3D_67t3u")
[node name="DOOR?4" parent="CSGBox3D" instance=ExtResource("2_vpnlr")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3.5, 5)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2.58627, 5)
material = SubResource("StandardMaterial3D_ej8w2")
[node name="DOOR?2" parent="CSGBox3D" instance=ExtResource("2_vpnlr")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3.5, -5)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2.58627, -5)
material = SubResource("StandardMaterial3D_oy7nu")
[node name="RemoveUnusedDoors" type="Node" parent="."]
diff --git a/src/map/dungeon/door/Door.tscn b/src/map/dungeon/door/Door.tscn
index b3edf2fe..26e5429d 100644
--- a/src/map/dungeon/door/Door.tscn
+++ b/src/map/dungeon/door/Door.tscn
@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://ckaw6wjmi0fom"]
[node name="DOOR" type="CSGBox3D"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.01493, 0)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.375916, 1.9271, 0)
operation = 2
-size = Vector3(2, 2, 1)
+size = Vector3(2.75183, 3.82434, 1)
diff --git a/src/map/dungeon/rooms/DungeonRoom.cs b/src/map/dungeon/rooms/DungeonRoom.cs
new file mode 100644
index 00000000..99ec9eb5
--- /dev/null
+++ b/src/map/dungeon/rooms/DungeonRoom.cs
@@ -0,0 +1,44 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using GameJamDungeon;
+using Godot;
+
+public interface IDungeonRoom : INode3D
+{
+ DungeonRoomLogic DungeonRoomLogic { get; }
+ public Marker3D PlayerSpawn { get; set; }
+}
+
+[Meta(typeof(IAutoNode))]
+public partial class DungeonRoom : Node3D, IDungeonRoom, IProvide
+{
+ public override void _Notification(int what) => this.Notify(what);
+
+ DungeonRoomLogic IProvide.Value() => DungeonRoomLogic;
+
+ [Dependency] public IGameRepo GameRepo => this.DependOn();
+
+ public DungeonRoomLogic DungeonRoomLogic { get; set; } = default!;
+
+ [Node] public Marker3D PlayerSpawn { get; set; } = default!;
+
+ public DungeonRoomLogic.IBinding DungeonRoomBinding { get; set; } = default!;
+
+ public void Setup()
+ {
+ DungeonRoomLogic = new DungeonRoomLogic();
+ DungeonRoomLogic.Set(this as IDungeonRoom);
+ DungeonRoomLogic.Set(GameRepo);
+ }
+
+ public void OnResolved()
+ {
+ DungeonRoomBinding = DungeonRoomLogic.Bind();
+
+ GameRepo.SetPlayerGlobalPosition(PlayerSpawn.GlobalPosition);
+
+ DungeonRoomLogic.Start();
+ this.Provide();
+ }
+}
diff --git a/src/map/dungeon/rooms/DungeonRoomLogic.cs b/src/map/dungeon/rooms/DungeonRoomLogic.cs
new file mode 100644
index 00000000..cf5efd36
--- /dev/null
+++ b/src/map/dungeon/rooms/DungeonRoomLogic.cs
@@ -0,0 +1,14 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public interface IDungeonRoomLogic : ILogicBlock;
+
+ [Meta, Id("dungeon_room_logic")]
+ [LogicBlock(typeof(State), Diagram = true)]
+ public partial class DungeonRoomLogic : LogicBlock, IDungeonRoomLogic
+ {
+ public override Transition GetInitialState() => To();
+ }
+}
diff --git a/src/map/dungeon/rooms/Room1.tscn b/src/map/dungeon/rooms/Room1.tscn
index d05448d8..413886c4 100644
--- a/src/map/dungeon/rooms/Room1.tscn
+++ b/src/map/dungeon/rooms/Room1.tscn
@@ -1,6 +1,7 @@
-[gd_scene load_steps=5 format=3 uid="uid://dhpwwqow1ahrc"]
+[gd_scene load_steps=6 format=3 uid="uid://dhpwwqow1ahrc"]
[ext_resource type="Script" path="res://addons/SimpleDungeons/DungeonRoom3D.gd" id="1_0tfda"]
+[ext_resource type="Script" path="res://src/map/dungeon/rooms/DungeonRoom.cs" id="1_ti7ur"]
[ext_resource type="PackedScene" uid="uid://ckaw6wjmi0fom" path="res://src/map/dungeon/door/Door.tscn" id="2_mdawx"]
[ext_resource type="Texture2D" uid="uid://bidlc5a6lft6" path="res://src/map/dungeon/textures/map_brickwall.jpg" id="2_rw3uc"]
@@ -12,6 +13,9 @@ uv1_triplanar = true
[node name="DungeonRoom3D" type="Node3D"]
script = ExtResource("1_0tfda")
+[node name="DungeonRoom" type="Node3D" parent="."]
+script = ExtResource("1_ti7ur")
+
[node name="CSGBox3D" type="CSGBox3D" parent="."]
material_override = SubResource("StandardMaterial3D_gt3ar")
use_collision = true
@@ -24,7 +28,11 @@ use_collision = true
size = Vector3(9, 9, 9)
[node name="DOOR" parent="CSGBox3D" instance=ExtResource("2_mdawx")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3.47376, 4.74571)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2.54039, 4.74571)
[node name="DOOR2" parent="CSGBox3D" instance=ExtResource("2_mdawx")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3.51619, -4.73548)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2.58282, -4.73548)
+
+[node name="PlayerSpawn" type="Marker3D" parent="."]
+unique_name_in_owner = true
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -4.23461, 0)
diff --git a/src/map/dungeon/rooms/state/DungeonRoomLogic.State.cs b/src/map/dungeon/rooms/state/DungeonRoomLogic.State.cs
new file mode 100644
index 00000000..87105238
--- /dev/null
+++ b/src/map/dungeon/rooms/state/DungeonRoomLogic.State.cs
@@ -0,0 +1,11 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class DungeonRoomLogic
+ {
+ [Meta]
+ public abstract partial record State : StateLogic;
+ }
+}
diff --git a/src/map/dungeon/rooms/state/states/DungeonRoomLogic.State.Idle.cs b/src/map/dungeon/rooms/state/states/DungeonRoomLogic.State.Idle.cs
new file mode 100644
index 00000000..021c23a5
--- /dev/null
+++ b/src/map/dungeon/rooms/state/states/DungeonRoomLogic.State.Idle.cs
@@ -0,0 +1,15 @@
+using Chickensoft.Introspection;
+
+namespace GameJamDungeon
+{
+ public partial class DungeonRoomLogic
+ {
+ public partial record State
+ {
+ [Meta, Id("dungeon_room_logic_state_idle")]
+ public partial record Idle : State
+ {
+ }
+ }
+ }
+}
diff --git a/src/menu/Menu.cs b/src/menu/Menu.cs
new file mode 100644
index 00000000..6a8ccc83
--- /dev/null
+++ b/src/menu/Menu.cs
@@ -0,0 +1,52 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using GameJamDungeon;
+using Godot;
+using System;
+
+public interface IMenu : IControl
+{
+ event Menu.NewGameEventHandler NewGame;
+ event Menu.QuitEventHandler Quit;
+}
+
+[Meta(typeof(IAutoNode))]
+public partial class Menu : Control, IMenu
+{
+ public override void _Notification(int what) => this.Notify(what);
+
+ [Node]
+ public IButton NewGameButton { get; set; } = default!;
+
+ [Node]
+ public IButton QuitButton { get; set; } = default!;
+
+ [Signal]
+ public delegate void NewGameEventHandler();
+ [Signal]
+ public delegate void QuitEventHandler();
+
+ public void OnReady()
+ {
+ NewGameButton.Pressed += OnNewGamePressed;
+ QuitButton.Pressed += OnQuitPressed;
+ NewGameButton.GrabFocus();
+ }
+
+ public void OnExitTree()
+ {
+ NewGameButton.Pressed -= OnNewGamePressed;
+ QuitButton.Pressed -= OnQuitPressed;
+ }
+
+ public void OnNewGamePressed() => EmitSignal(SignalName.NewGame);
+
+ public void OnQuitPressed() => EmitSignal(SignalName.Quit);
+
+ public override void _UnhandledInput(InputEvent @event)
+ {
+ if (Input.IsActionJustPressed(GameInputs.Attack))
+ OnNewGamePressed();
+ }
+}
diff --git a/src/menu/splash/Splash.cs b/src/menu/splash/Splash.cs
new file mode 100644
index 00000000..5ed4e838
--- /dev/null
+++ b/src/menu/splash/Splash.cs
@@ -0,0 +1,30 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using Godot;
+
+namespace GameJamDungeon
+{
+ public interface ISplash : IControl;
+
+ [Meta(typeof(IAutoNode))]
+ public partial class Splash : Control, ISplash
+ {
+ public override void _Notification(int what) => this.Notify(what);
+
+ [Dependency]
+ public IAppRepo AppRepo => this.DependOn();
+
+ [Node]
+ public IAnimationPlayer AnimationPlayer { get; set; } = default!;
+
+ public void OnReady() =>
+ AnimationPlayer.AnimationFinished += OnAnimationFinished;
+
+ public void OnExitTree()
+ => AnimationPlayer.AnimationFinished -= OnAnimationFinished;
+
+ public void OnAnimationFinished(StringName name)
+ => AppRepo.SkipSplashScreen();
+ }
+}
diff --git a/src/minimap/MiniMap.cs b/src/minimap/MiniMap.cs
new file mode 100644
index 00000000..214c8646
--- /dev/null
+++ b/src/minimap/MiniMap.cs
@@ -0,0 +1,6 @@
+using Godot;
+using System;
+
+public partial class MiniMap : Control
+{
+}
diff --git a/src/player/Player.cs b/src/player/Player.cs
new file mode 100644
index 00000000..d9576817
--- /dev/null
+++ b/src/player/Player.cs
@@ -0,0 +1,130 @@
+using Chickensoft.AutoInject;
+using Chickensoft.GodotNodeInterfaces;
+using Chickensoft.Introspection;
+using Godot;
+using System;
+
+namespace GameJamDungeon
+{
+ public interface IPlayer : ICharacterBody3D
+ {
+ PlayerLogic PlayerLogic { get; }
+
+ PlayerData PlayerData { get; }
+
+ public Vector3 GetGlobalInputVector();
+ }
+
+ [Meta(typeof(IAutoNode))]
+ public partial class Player : CharacterBody3D, IPlayer, IProvide
+ {
+ public override void _Notification(int what) => this.Notify(what);
+
+ PlayerLogic IProvide.Value() => PlayerLogic;
+
+ [Dependency] public IAppRepo AppRepo => this.DependOn();
+ [Dependency] public IGameRepo GameRepo => this.DependOn();
+
+ /// Rotation speed (quaternions?/sec).
+ [Export(PropertyHint.Range, "0, 100, 0.1")]
+ public float RotationSpeed { get; set; } = 0.5f;
+
+ /// Player speed (meters/sec).
+ [Export(PropertyHint.Range, "0, 100, 0.1")]
+ public float MoveSpeed { get; set; } = 2f;
+
+ /// Player speed (meters^2/sec).
+ [Export(PropertyHint.Range, "0, 100, 0.1")]
+ public float Acceleration { get; set; } = 1f;
+
+ public PlayerLogic.Settings Settings { get; set; } = default!;
+
+ public PlayerLogic PlayerLogic { get; set; } = default!;
+
+ public PlayerData PlayerData { get; set; } = default!;
+
+ public PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
+
+ public void Initialize()
+ {
+
+ }
+
+ public void Setup()
+ {
+ Settings = new PlayerLogic.Settings(
+ RotationSpeed,
+ MoveSpeed);
+
+ PlayerLogic = new PlayerLogic();
+ PlayerLogic.Set(this as IPlayer);
+ PlayerLogic.Set(Settings);
+ PlayerLogic.Set(AppRepo);
+ PlayerLogic.Set(GameRepo);
+ PlayerLogic.Set(PlayerData);
+
+ GameRepo.PlayerGlobalPosition.Sync += OnPlayerPositionUpdated;
+ }
+
+ private void OnPlayerPositionUpdated(Vector3 globalPosition)
+ {
+ GlobalPosition = globalPosition;
+ }
+
+ public void OnResolved()
+ {
+ PlayerBinding = PlayerLogic.Bind();
+
+ PlayerBinding
+ .Handle((in PlayerLogic.Output.MovementComputed output) =>
+ {
+ Transform = Transform with { Basis = output.Rotation };
+ Velocity = output.Velocity;
+ })
+ .Handle((in PlayerLogic.Output.Animations.Attack output) =>
+ {
+ });
+
+ this.Provide();
+ PlayerLogic.Start();
+ }
+
+ public void OnReady()
+ {
+ SetPhysicsProcess(true);
+ }
+
+ public void OnPhysicsProcess(double delta)
+ {
+ PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta));
+
+ var attackIsPressed = Input.IsActionJustPressed(GameInputs.Attack);
+ if (attackIsPressed)
+ PlayerLogic.Input(new PlayerLogic.Input.Attack());
+
+ MoveAndSlide();
+
+ PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition));
+ }
+
+ public Vector3 GetGlobalInputVector()
+ {
+ var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown);
+
+ var input = new Vector3
+ {
+ X = rawInput.X,
+ Z = rawInput.Y
+ };
+ return input with { Y = 0f };
+ }
+
+ public void OnExitTree()
+ {
+ PlayerLogic.Stop();
+ AppRepo.Dispose();
+ GameRepo.Dispose();
+ PlayerBinding.Dispose();
+ }
+ }
+}
diff --git a/src/player/Player.tscn b/src/player/Player.tscn
new file mode 100644
index 00000000..12fd470d
--- /dev/null
+++ b/src/player/Player.tscn
@@ -0,0 +1,24 @@
+[gd_scene load_steps=3 format=3 uid="uid://cfecvvav8kkp6"]
+
+[ext_resource type="Script" path="res://src/player/Player.cs" id="1_xcol5"]
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_dw45s"]
+height = 1.2
+
+[node name="Player" type="CharacterBody3D"]
+motion_mode = 1
+script = ExtResource("1_xcol5")
+RotationSpeed = 0.025
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.937567, 0)
+shape = SubResource("CapsuleShape3D_dw45s")
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19694, 0)
+
+[node name="OmniLight3D" type="OmniLight3D" parent="."]
+omni_range = 73.156
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
+unique_name_in_owner = true
diff --git a/src/player/PlayerData.cs b/src/player/PlayerData.cs
new file mode 100644
index 00000000..b920bff5
--- /dev/null
+++ b/src/player/PlayerData.cs
@@ -0,0 +1,22 @@
+using Chickensoft.Serialization;
+using Godot;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace GameJamDungeon
+{
+ public partial record PlayerData
+ {
+ [Save("global_transform")]
+ public required Transform3D GlobalTransform { get; init; }
+
+ [Save("state_machine")]
+ public required PlayerLogic StateMachine { get; init; }
+
+ [Save("PlayerEquippedSword")]
+ public required InventoryItem EquippedWeapon { get; set; }
+
+ [Save("PlayerInventory")]
+ public required IEnumerable Inventory { get; set; }
+ }
+}
diff --git a/src/player/state/PlayerLogic.Input.cs b/src/player/state/PlayerLogic.Input.cs
new file mode 100644
index 00000000..f003f6a9
--- /dev/null
+++ b/src/player/state/PlayerLogic.Input.cs
@@ -0,0 +1,20 @@
+using Godot;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public static class Input
+ {
+ public readonly record struct PhysicsTick(double Delta);
+
+ public readonly record struct Moved(Vector3 GlobalPosition);
+
+ public readonly record struct Enable;
+
+ public readonly record struct Attack;
+
+ public readonly record struct AttackAnimationFinished;
+ }
+ }
+}
diff --git a/src/player/state/PlayerLogic.Output.cs b/src/player/state/PlayerLogic.Output.cs
new file mode 100644
index 00000000..5c561c51
--- /dev/null
+++ b/src/player/state/PlayerLogic.Output.cs
@@ -0,0 +1,17 @@
+using Godot;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public static class Output
+ {
+ public static class Animations
+ {
+ public readonly record struct Attack;
+ }
+
+ public readonly record struct MovementComputed(Basis Rotation, Vector3 Velocity);
+ }
+ }
+}
diff --git a/src/player/state/PlayerLogic.Settings.cs b/src/player/state/PlayerLogic.Settings.cs
new file mode 100644
index 00000000..06561015
--- /dev/null
+++ b/src/player/state/PlayerLogic.Settings.cs
@@ -0,0 +1,9 @@
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public record Settings(
+ float RotationSpeed,
+ float MoveSpeed);
+ }
+}
diff --git a/src/player/state/PlayerLogic.State.cs b/src/player/state/PlayerLogic.State.cs
new file mode 100644
index 00000000..091ee2ab
--- /dev/null
+++ b/src/player/state/PlayerLogic.State.cs
@@ -0,0 +1,11 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ [Meta]
+ public abstract partial record State : StateLogic;
+ }
+}
diff --git a/src/player/state/PlayerLogic.cs b/src/player/state/PlayerLogic.cs
new file mode 100644
index 00000000..c8fd7b7e
--- /dev/null
+++ b/src/player/state/PlayerLogic.cs
@@ -0,0 +1,14 @@
+using Chickensoft.Introspection;
+using Chickensoft.LogicBlocks;
+
+namespace GameJamDungeon
+{
+ public interface IPlayerLogic : ILogicBlock;
+
+ [Meta, Id("player_logic")]
+ [LogicBlock(typeof(State), Diagram = true)]
+ public partial class PlayerLogic : LogicBlock, IPlayerLogic
+ {
+ public override Transition GetInitialState() => To();
+ }
+}
diff --git a/src/player/state/PlayerLogic.g.puml b/src/player/state/PlayerLogic.g.puml
new file mode 100644
index 00000000..47e30ecf
--- /dev/null
+++ b/src/player/state/PlayerLogic.g.puml
@@ -0,0 +1,20 @@
+@startuml PlayerLogic
+state "PlayerLogic State" as GameJam2024Practice_PlayerLogic_State {
+ state "Alive" as GameJam2024Practice_PlayerLogic_State_Alive {
+ state "Idle" as GameJam2024Practice_PlayerLogic_State_Idle
+ state "Attacking" as GameJam2024Practice_PlayerLogic_State_Attacking
+ }
+ state "Disabled" as GameJam2024Practice_PlayerLogic_State_Disabled
+}
+
+GameJam2024Practice_PlayerLogic_State_Alive --> GameJam2024Practice_PlayerLogic_State_Alive : Moved
+GameJam2024Practice_PlayerLogic_State_Alive --> GameJam2024Practice_PlayerLogic_State_Alive : PhysicsTick
+GameJam2024Practice_PlayerLogic_State_Attacking --> GameJam2024Practice_PlayerLogic_State_Idle : AttackAnimationFinished
+GameJam2024Practice_PlayerLogic_State_Disabled --> GameJam2024Practice_PlayerLogic_State_Idle : Enable
+GameJam2024Practice_PlayerLogic_State_Idle --> GameJam2024Practice_PlayerLogic_State_Attacking : Attack
+
+GameJam2024Practice_PlayerLogic_State_Alive : OnPhysicsTick → MovementComputed
+GameJam2024Practice_PlayerLogic_State_Idle : OnAttack → Attack
+
+[*] --> GameJam2024Practice_PlayerLogic_State_Disabled
+@enduml
\ No newline at end of file
diff --git a/src/player/state/states/PlayerLogic.State.Alive.Attacking.cs b/src/player/state/states/PlayerLogic.State.Alive.Attacking.cs
new file mode 100644
index 00000000..311162a1
--- /dev/null
+++ b/src/player/state/states/PlayerLogic.State.Alive.Attacking.cs
@@ -0,0 +1,19 @@
+using Chickensoft.Introspection;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public partial record State
+ {
+ [Meta]
+ public partial record Attacking : Alive, IGet
+ {
+ public Transition On(in Input.AttackAnimationFinished input)
+ {
+ return To();
+ }
+ }
+ }
+ }
+}
diff --git a/src/player/state/states/PlayerLogic.State.Alive.Idle.cs b/src/player/state/states/PlayerLogic.State.Alive.Idle.cs
new file mode 100644
index 00000000..b77303a5
--- /dev/null
+++ b/src/player/state/states/PlayerLogic.State.Alive.Idle.cs
@@ -0,0 +1,23 @@
+using Chickensoft.Introspection;
+using Godot;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public abstract partial record State
+ {
+ [Meta, Id("player_logic_state_alive_idle")]
+ public partial record Idle : Alive, IGet
+ {
+
+ public virtual Transition On(in Input.Attack input)
+ {
+ GD.Print("Attacking...");
+ Output(new Output.Animations.Attack());
+ return To();
+ }
+ }
+ }
+ }
+}
diff --git a/src/player/state/states/PlayerLogic.State.Alive.cs b/src/player/state/states/PlayerLogic.State.Alive.cs
new file mode 100644
index 00000000..63fc7c05
--- /dev/null
+++ b/src/player/state/states/PlayerLogic.State.Alive.cs
@@ -0,0 +1,41 @@
+using Chickensoft.Introspection;
+using Godot;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public partial record State
+ {
+ [Meta, Id("player_logic_alive")]
+ public abstract partial record Alive : State, IGet, IGet
+ {
+ public virtual Transition On(in Input.PhysicsTick input)
+ {
+ var player = Get();
+ var settings = Get();
+
+ var rawInput = player.GetGlobalInputVector();
+
+ var transform = player.Transform;
+ transform.Basis = new Basis(Vector3.Up, settings.RotationSpeed * -rawInput.X) * transform.Basis;
+ var velocity = player.Basis * new Vector3(0, 0, rawInput.Z) * settings.MoveSpeed;
+
+ if (Godot.Input.IsActionPressed(GameInputs.Sprint))
+ velocity *= 3;
+
+ Output(new Output.MovementComputed(transform.Basis, velocity));
+
+ return ToSelf();
+ }
+
+ public virtual Transition On(in Input.Moved input)
+ {
+ var gameRepo = Get();
+ gameRepo.SetPlayerGlobalPosition(input.GlobalPosition);
+ return ToSelf();
+ }
+ }
+ }
+ }
+}
diff --git a/src/player/state/states/PlayerLogic.State.Disabled.cs b/src/player/state/states/PlayerLogic.State.Disabled.cs
new file mode 100644
index 00000000..088d7a58
--- /dev/null
+++ b/src/player/state/states/PlayerLogic.State.Disabled.cs
@@ -0,0 +1,24 @@
+using Chickensoft.Introspection;
+
+namespace GameJamDungeon
+{
+ public partial class PlayerLogic
+ {
+ public abstract partial record State
+ {
+ [Meta, Id("player_logic_state_disabled")]
+ public partial record Disabled : State, IGet
+ {
+ public Disabled()
+ {
+ OnAttach(() => Get().GameEntered += OnGameEntered);
+ OnDetach(() => Get().GameEntered -= OnGameEntered);
+ }
+
+ public Transition On(in Input.Enable input) => To();
+
+ public void OnGameEntered() => Input(new Input.Enable());
+ }
+ }
+ }
+}
diff --git a/src/utils/FpsCounter.cs b/src/utils/FpsCounter.cs
new file mode 100644
index 00000000..83222754
--- /dev/null
+++ b/src/utils/FpsCounter.cs
@@ -0,0 +1,9 @@
+using Godot;
+
+public partial class FpsCounter : Label
+{
+ public override void _Process(double delta)
+ {
+ this.Text = Engine.GetFramesPerSecond().ToString();
+ }
+}
diff --git a/src/utils/GameInputs.cs b/src/utils/GameInputs.cs
new file mode 100644
index 00000000..a087188a
--- /dev/null
+++ b/src/utils/GameInputs.cs
@@ -0,0 +1,7 @@
+using Godot;
+
+namespace GameJamDungeon
+{
+ [InputMap]
+ public partial class GameInputs;
+}
diff --git a/src/utils/Instantiator.cs b/src/utils/Instantiator.cs
new file mode 100644
index 00000000..d0717c71
--- /dev/null
+++ b/src/utils/Instantiator.cs
@@ -0,0 +1,40 @@
+using Godot;
+
+namespace GameJamDungeon
+{
+ ///
+ /// Utility class that loads and instantiates scenes.
+ ///
+ public interface IInstantiator
+ {
+ /// Scene tree.
+ public SceneTree SceneTree { get; }
+
+ ///
+ /// Loads and instantiates the given scene.
+ ///
+ /// Path to the scene.
+ /// Type of the scene's root.
+ /// Instance of the scene.
+ T LoadAndInstantiate(string path) where T : Node;
+ }
+
+ ///
+ /// Utility class that loads and instantiates scenes.
+ ///
+ public class Instantiator : IInstantiator
+ {
+ public SceneTree SceneTree { get; }
+
+ public Instantiator(SceneTree sceneTree)
+ {
+ SceneTree = sceneTree;
+ }
+
+ public T LoadAndInstantiate(string path) where T : Node
+ {
+ var scene = GD.Load(path);
+ return scene.Instantiate();
+ }
+ }
+}