diff --git a/showcase_games/circuit_logic/icon.svg b/showcase_games/circuit_logic/icon.svg
new file mode 100644
index 00000000..e41f7477
--- /dev/null
+++ b/showcase_games/circuit_logic/icon.svg
@@ -0,0 +1,23 @@
+
diff --git a/showcase_games/circuit_logic/project.godot b/showcase_games/circuit_logic/project.godot
new file mode 100644
index 00000000..91823480
--- /dev/null
+++ b/showcase_games/circuit_logic/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="Circuit Logic"
+config/description="A minimalist puzzle game about completing circuits. Rotate tiles to connect power sources to outputs. Simple to learn, challenging to master."
+run/main_scene="res://scenes/main_menu.tscn"
+config/features=PackedStringArray("4.7", "Forward Plus")
+config/icon="res://icon.svg"
+
+[autoload]
+
+PuzzleManager="*res://scripts/autoload/puzzle_manager.gd"
+ProgressManager="*res://scripts/autoload/progress_manager.gd"
+
+[display]
+
+window/size/viewport_width=1280
+window/size/viewport_height=720
+window/stretch/mode="canvas_items"
+window/stretch/aspect="keep"
+
+[input]
+
+click={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+right_click={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+reset_level={
+"deadzone": 0.5,
+"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":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
+]
+}
+undo={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null)
+]
+}
+pause={
+"deadzone": 0.5,
+"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":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+
+[rendering]
+
+renderer/rendering_method="forward_plus"
+environment/defaults/default_clear_color=Color(0.12, 0.14, 0.18, 1)
diff --git a/showcase_games/circuit_logic/scenes/game.tscn b/showcase_games/circuit_logic/scenes/game.tscn
new file mode 100644
index 00000000..cb4aa163
--- /dev/null
+++ b/showcase_games/circuit_logic/scenes/game.tscn
@@ -0,0 +1,149 @@
+[gd_scene load_steps=2 format=3 uid="uid://circuit_game"]
+
+[ext_resource type="Script" path="res://scripts/game/game_controller.gd" id="1_game"]
+
+[node name="Game" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("1_game")
+
+[node name="Background" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.12, 0.14, 0.18, 1)
+
+[node name="GridContainer" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -250.0
+offset_top = -250.0
+offset_right = 250.0
+offset_bottom = 250.0
+
+[node name="UI" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="TopBar" type="HBoxContainer" parent="UI"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_top = 10.0
+offset_bottom = 60.0
+theme_override_constants/separation = 20
+
+[node name="BackButton" type="Button" parent="UI/TopBar"]
+custom_minimum_size = Vector2(100, 40)
+layout_mode = 2
+text = "< BACK"
+
+[node name="LevelName" type="Label" parent="UI/TopBar"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_colors/font_color = Color(0.8, 0.9, 1, 1)
+theme_override_font_sizes/font_size = 24
+text = "Level 1: First Steps"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="MovesLabel" type="Label" parent="UI/TopBar"]
+custom_minimum_size = Vector2(120, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(0.6, 0.7, 0.8, 1)
+theme_override_font_sizes/font_size = 20
+text = "Moves: 0"
+horizontal_alignment = 2
+vertical_alignment = 1
+
+[node name="BottomBar" type="HBoxContainer" parent="UI"]
+layout_mode = 1
+anchors_preset = 12
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = -60.0
+offset_bottom = -10.0
+theme_override_constants/separation = 20
+
+[node name="ResetButton" type="Button" parent="UI/BottomBar"]
+custom_minimum_size = Vector2(120, 40)
+layout_mode = 2
+text = "RESET [R]"
+
+[node name="UndoButton" type="Button" parent="UI/BottomBar"]
+custom_minimum_size = Vector2(120, 40)
+layout_mode = 2
+text = "UNDO [Z]"
+
+[node name="Spacer" type="Control" parent="UI/BottomBar"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ParLabel" type="Label" parent="UI/BottomBar"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.85, 0.3, 0.8)
+theme_override_font_sizes/font_size = 18
+text = "Par: 5 moves"
+horizontal_alignment = 2
+vertical_alignment = 1
+
+[node name="WinPanel" type="PanelContainer" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -180.0
+offset_top = -140.0
+offset_right = 180.0
+offset_bottom = 140.0
+
+[node name="VBox" type="VBoxContainer" parent="WinPanel"]
+layout_mode = 2
+theme_override_constants/separation = 15
+
+[node name="Title" type="Label" parent="WinPanel/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.3, 0.8, 0.5, 1)
+theme_override_font_sizes/font_size = 36
+text = "SOLVED!"
+horizontal_alignment = 1
+
+[node name="StarsLabel" type="Label" parent="WinPanel/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.85, 0.3, 1)
+theme_override_font_sizes/font_size = 32
+text = "★ ★ ★"
+horizontal_alignment = 1
+
+[node name="MovesResult" type="Label" parent="WinPanel/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.7, 0.8, 0.9, 1)
+theme_override_font_sizes/font_size = 18
+text = "Completed in 5 moves"
+horizontal_alignment = 1
+
+[node name="NextButton" type="Button" parent="WinPanel/VBox"]
+custom_minimum_size = Vector2(200, 45)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "NEXT LEVEL"
+
+[node name="MenuButton" type="Button" parent="WinPanel/VBox"]
+custom_minimum_size = Vector2(160, 35)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "MENU"
diff --git a/showcase_games/circuit_logic/scenes/level_select.tscn b/showcase_games/circuit_logic/scenes/level_select.tscn
new file mode 100644
index 00000000..11cfadc4
--- /dev/null
+++ b/showcase_games/circuit_logic/scenes/level_select.tscn
@@ -0,0 +1,79 @@
+[gd_scene load_steps=2 format=3 uid="uid://circuit_level_select"]
+
+[ext_resource type="Script" path="res://scripts/ui/level_select.gd" id="1_select"]
+
+[node name="LevelSelect" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("1_select")
+
+[node name="Background" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.12, 0.14, 0.18, 1)
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 40.0
+offset_top = 20.0
+offset_right = -40.0
+offset_bottom = -20.0
+theme_override_constants/separation = 20
+
+[node name="Header" type="HBoxContainer" parent="VBox"]
+layout_mode = 2
+
+[node name="BackButton" type="Button" parent="VBox/Header"]
+custom_minimum_size = Vector2(100, 40)
+layout_mode = 2
+text = "< BACK"
+
+[node name="Title" type="Label" parent="VBox/Header"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_colors/font_color = Color(0.8, 0.9, 1, 1)
+theme_override_font_sizes/font_size = 32
+text = "SELECT LEVEL"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="TotalStars" type="Label" parent="VBox/Header"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.85, 0.3, 1)
+theme_override_font_sizes/font_size = 20
+text = "★ 0 / 30"
+horizontal_alignment = 2
+vertical_alignment = 1
+
+[node name="ChapterTabs" type="HBoxContainer" parent="VBox"]
+layout_mode = 2
+alignment = 1
+
+[node name="Chapter1" type="Button" parent="VBox/ChapterTabs"]
+custom_minimum_size = Vector2(150, 40)
+layout_mode = 2
+text = "CHAPTER 1"
+
+[node name="Chapter2" type="Button" parent="VBox/ChapterTabs"]
+custom_minimum_size = Vector2(150, 40)
+layout_mode = 2
+text = "CHAPTER 2"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="LevelGrid" type="GridContainer" parent="VBox/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/h_separation = 20
+theme_override_constants/v_separation = 20
+columns = 5
diff --git a/showcase_games/circuit_logic/scenes/main_menu.tscn b/showcase_games/circuit_logic/scenes/main_menu.tscn
new file mode 100644
index 00000000..60c14d12
--- /dev/null
+++ b/showcase_games/circuit_logic/scenes/main_menu.tscn
@@ -0,0 +1,97 @@
+[gd_scene load_steps=2 format=3 uid="uid://circuit_main_menu"]
+
+[ext_resource type="Script" path="res://scripts/ui/main_menu.gd" id="1_menu"]
+
+[node name="MainMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("1_menu")
+
+[node name="Background" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.12, 0.14, 0.18, 1)
+
+[node name="CircuitPattern" type="Control" parent="Background"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -180.0
+offset_top = -160.0
+offset_right = 180.0
+offset_bottom = 160.0
+theme_override_constants/separation = 20
+
+[node name="Title" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.3, 0.8, 0.5, 1)
+theme_override_font_sizes/font_size = 56
+text = "CIRCUIT LOGIC"
+horizontal_alignment = 1
+
+[node name="Subtitle" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.6, 0.7, 0.8, 0.8)
+theme_override_font_sizes/font_size = 16
+text = "Complete the circuit. Power the outputs."
+horizontal_alignment = 1
+
+[node name="StarsLabel" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.85, 0.3, 1)
+theme_override_font_sizes/font_size = 20
+text = "★ 0 / 30"
+horizontal_alignment = 1
+
+[node name="Spacer" type="Control" parent="VBox"]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="PlayButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(280, 55)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 24
+text = "▶ PLAY"
+
+[node name="LevelSelectButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(240, 45)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 18
+text = "LEVEL SELECT"
+
+[node name="QuitButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(180, 40)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 16
+text = "QUIT"
+
+[node name="Credits" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -150.0
+offset_top = -30.0
+offset_right = 150.0
+theme_override_colors/font_color = Color(1, 1, 1, 0.4)
+theme_override_font_sizes/font_size = 12
+text = "Made with AeThex Engine"
+horizontal_alignment = 1
diff --git a/showcase_games/circuit_logic/scripts/autoload/progress_manager.gd b/showcase_games/circuit_logic/scripts/autoload/progress_manager.gd
new file mode 100644
index 00000000..fdca7783
--- /dev/null
+++ b/showcase_games/circuit_logic/scripts/autoload/progress_manager.gd
@@ -0,0 +1,330 @@
+extends Node
+## Progress Manager - Tracks level completion and stars
+
+signal level_completed(level_id: int, stars: int)
+signal chapter_unlocked(chapter: int)
+
+const SAVE_PATH = "user://circuit_logic_progress.json"
+
+# Progress data
+var completed_levels: Dictionary = {} # level_id -> {stars, moves, time}
+var current_chapter: int = 1
+var unlocked_chapters: Array[int] = [1]
+
+# Level definitions
+var levels: Array[Dictionary] = []
+
+func _ready() -> void:
+ _define_levels()
+ load_progress()
+
+func _define_levels() -> void:
+ # Chapter 1: Basics (5 levels)
+ levels.append(_create_tutorial_1())
+ levels.append(_create_tutorial_2())
+ levels.append(_create_tutorial_3())
+ levels.append(_create_level_4())
+ levels.append(_create_level_5())
+
+ # Chapter 2: Corners (5 levels)
+ levels.append(_create_level_6())
+ levels.append(_create_level_7())
+ levels.append(_create_level_8())
+ levels.append(_create_level_9())
+ levels.append(_create_level_10())
+
+func _create_tutorial_1() -> Dictionary:
+ # Simple straight line - just one rotation
+ return {
+ "id": 1,
+ "name": "First Steps",
+ "chapter": 1,
+ "size": Vector2i(3, 3),
+ "par_moves": 1,
+ "tiles": [
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 0},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_tutorial_2() -> Dictionary:
+ return {
+ "id": 2,
+ "name": "Turn Around",
+ "chapter": 1,
+ "size": Vector2i(3, 3),
+ "par_moves": 2,
+ "tiles": [
+ {"x": 0, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_tutorial_3() -> Dictionary:
+ return {
+ "id": 3,
+ "name": "The T",
+ "chapter": 1,
+ "size": Vector2i(3, 3),
+ "par_moves": 3,
+ "tiles": [
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_level_4() -> Dictionary:
+ return {
+ "id": 4,
+ "name": "Crossroads",
+ "chapter": 1,
+ "size": Vector2i(5, 5),
+ "par_moves": 4,
+ "tiles": [
+ {"x": 2, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 0},
+ {"x": 2, "y": 2, "type": PuzzleManager.TileType.CROSS, "rotation": 0},
+ {"x": 0, "y": 2, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 4, "y": 2, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 1, "y": 2, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ ]
+ }
+
+func _create_level_5() -> Dictionary:
+ return {
+ "id": 5,
+ "name": "Zig Zag",
+ "chapter": 1,
+ "size": Vector2i(4, 4),
+ "par_moves": 5,
+ "tiles": [
+ {"x": 0, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 2, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_level_6() -> Dictionary:
+ return {
+ "id": 6,
+ "name": "Two Paths",
+ "chapter": 2,
+ "size": Vector2i(5, 5),
+ "par_moves": 6,
+ "tiles": [
+ {"x": 0, "y": 2, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 1, "y": 2, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 1, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 2, "y": 3, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 1},
+ {"x": 4, "y": 2, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_level_7() -> Dictionary:
+ return {
+ "id": 7,
+ "name": "Spiral",
+ "chapter": 2,
+ "size": Vector2i(5, 5),
+ "par_moves": 8,
+ "tiles": [
+ {"x": 2, "y": 2, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 3, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 3, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 2, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 1, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 1, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ ]
+ }
+
+func _create_level_8() -> Dictionary:
+ return {
+ "id": 8,
+ "name": "Grid Lock",
+ "chapter": 2,
+ "size": Vector2i(5, 5),
+ "par_moves": 10,
+ "tiles": [
+ {"x": 0, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 4, "y": 4, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 0},
+ {"x": 2, "y": 0, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 2, "y": 1, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 2, "y": 2, "type": PuzzleManager.TileType.CROSS, "rotation": 0},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 3, "y": 3, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 4, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ ]
+ }
+
+func _create_level_9() -> Dictionary:
+ return {
+ "id": 9,
+ "name": "Twin Outputs",
+ "chapter": 2,
+ "size": Vector2i(6, 4),
+ "par_moves": 8,
+ "tiles": [
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 5, "y": 0, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 5, "y": 3, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 1, "y": 1, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 2, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 2, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 3, "y": 0, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 2, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 2},
+ {"x": 4, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 4, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 1, "y": 2, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ ]
+ }
+
+func _create_level_10() -> Dictionary:
+ return {
+ "id": 10,
+ "name": "The Challenge",
+ "chapter": 2,
+ "size": Vector2i(6, 6),
+ "par_moves": 12,
+ "tiles": [
+ {"x": 0, "y": 0, "type": PuzzleManager.TileType.SOURCE, "rotation": 0, "locked": true},
+ {"x": 5, "y": 5, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 0, "y": 5, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 5, "y": 0, "type": PuzzleManager.TileType.OUTPUT, "rotation": 0, "locked": true},
+ {"x": 1, "y": 0, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 2, "y": 0, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 3, "y": 0, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 0},
+ {"x": 4, "y": 0, "type": PuzzleManager.TileType.CORNER, "rotation": 1},
+ {"x": 0, "y": 1, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 0, "y": 2, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 0},
+ {"x": 0, "y": 3, "type": PuzzleManager.TileType.T_JUNCTION, "rotation": 3},
+ {"x": 0, "y": 4, "type": PuzzleManager.TileType.CORNER, "rotation": 3},
+ {"x": 3, "y": 3, "type": PuzzleManager.TileType.CROSS, "rotation": 0},
+ {"x": 3, "y": 5, "type": PuzzleManager.TileType.CORNER, "rotation": 0},
+ {"x": 4, "y": 5, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 1},
+ {"x": 5, "y": 3, "type": PuzzleManager.TileType.CORNER, "rotation": 2},
+ {"x": 5, "y": 4, "type": PuzzleManager.TileType.STRAIGHT, "rotation": 0},
+ ]
+ }
+
+func get_level(level_id: int) -> Dictionary:
+ for level in levels:
+ if level.id == level_id:
+ return level
+ return {}
+
+func get_levels_for_chapter(chapter: int) -> Array[Dictionary]:
+ var result: Array[Dictionary] = []
+ for level in levels:
+ if level.chapter == chapter:
+ result.append(level)
+ return result
+
+func complete_level(level_id: int, moves: int, time: float) -> int:
+ var level = get_level(level_id)
+ if level.is_empty():
+ return 0
+
+ # Calculate stars
+ var stars = 1 # Base star for completion
+ if moves <= level.par_moves:
+ stars = 3
+ elif moves <= level.par_moves * 1.5:
+ stars = 2
+
+ # Save progress
+ var previous = completed_levels.get(level_id, {"stars": 0})
+ if stars > previous.get("stars", 0) or not completed_levels.has(level_id):
+ completed_levels[level_id] = {
+ "stars": stars,
+ "moves": moves,
+ "time": time,
+ }
+
+ # Check chapter unlock
+ _check_chapter_unlock()
+
+ save_progress()
+ level_completed.emit(level_id, stars)
+ return stars
+
+func _check_chapter_unlock() -> void:
+ # Unlock chapter 2 if all chapter 1 levels completed
+ var chapter1_complete = true
+ for level in get_levels_for_chapter(1):
+ if not completed_levels.has(level.id):
+ chapter1_complete = false
+ break
+
+ if chapter1_complete and 2 not in unlocked_chapters:
+ unlocked_chapters.append(2)
+ chapter_unlocked.emit(2)
+
+func is_level_unlocked(level_id: int) -> bool:
+ var level = get_level(level_id)
+ if level.is_empty():
+ return false
+
+ # First level always unlocked
+ if level_id == 1:
+ return true
+
+ # Unlock if previous level completed
+ return completed_levels.has(level_id - 1)
+
+func get_total_stars() -> int:
+ var total = 0
+ for data in completed_levels.values():
+ total += data.get("stars", 0)
+ return total
+
+func save_progress() -> void:
+ var data = {
+ "completed_levels": completed_levels,
+ "unlocked_chapters": unlocked_chapters,
+ }
+ var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
+ if file:
+ file.store_string(JSON.stringify(data))
+
+func load_progress() -> void:
+ if not FileAccess.file_exists(SAVE_PATH):
+ return
+
+ var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if not file:
+ return
+
+ var json = JSON.new()
+ if json.parse(file.get_as_text()) == OK:
+ var data = json.get_data()
+ completed_levels = data.get("completed_levels", {})
+ # Convert string keys back to ints
+ var converted = {}
+ for key in completed_levels.keys():
+ converted[int(key)] = completed_levels[key]
+ completed_levels = converted
+
+ var chapters = data.get("unlocked_chapters", [1])
+ unlocked_chapters.clear()
+ for c in chapters:
+ unlocked_chapters.append(int(c))
diff --git a/showcase_games/circuit_logic/scripts/autoload/puzzle_manager.gd b/showcase_games/circuit_logic/scripts/autoload/puzzle_manager.gd
new file mode 100644
index 00000000..2f4bc65e
--- /dev/null
+++ b/showcase_games/circuit_logic/scripts/autoload/puzzle_manager.gd
@@ -0,0 +1,208 @@
+extends Node
+## Puzzle Manager - Handles puzzle logic, tile connections, and win conditions
+
+signal puzzle_solved
+signal power_changed(tile: Node2D, powered: bool)
+signal move_made(moves: int)
+
+# Tile types
+enum TileType {
+ EMPTY,
+ STRAIGHT, # | or -
+ CORNER, # L shape
+ T_JUNCTION, # T shape
+ CROSS, # + shape
+ SOURCE, # Power source (always powered)
+ OUTPUT, # Target (must be powered to win)
+}
+
+# Connection directions (bitflags)
+const DIR_UP = 1
+const DIR_RIGHT = 2
+const DIR_DOWN = 4
+const DIR_LEFT = 8
+
+# Base connections for each tile type (rotation 0)
+const TILE_CONNECTIONS = {
+ TileType.EMPTY: 0,
+ TileType.STRAIGHT: DIR_UP | DIR_DOWN,
+ TileType.CORNER: DIR_UP | DIR_RIGHT,
+ TileType.T_JUNCTION: DIR_UP | DIR_RIGHT | DIR_DOWN,
+ TileType.CROSS: DIR_UP | DIR_RIGHT | DIR_DOWN | DIR_LEFT,
+ TileType.SOURCE: DIR_UP | DIR_RIGHT | DIR_DOWN | DIR_LEFT,
+ TileType.OUTPUT: DIR_UP | DIR_RIGHT | DIR_DOWN | DIR_LEFT,
+}
+
+var current_puzzle: Dictionary = {}
+var grid_size: Vector2i = Vector2i(5, 5)
+var tiles: Array = [] # 2D array of tile data
+var move_count: int = 0
+var undo_stack: Array = []
+
+func _ready() -> void:
+ pass
+
+func load_puzzle(puzzle_data: Dictionary) -> void:
+ current_puzzle = puzzle_data
+ grid_size = puzzle_data.get("size", Vector2i(5, 5))
+ move_count = 0
+ undo_stack.clear()
+
+ # Initialize tiles array
+ tiles = []
+ for y in grid_size.y:
+ var row = []
+ for x in grid_size.x:
+ row.append({
+ "type": TileType.EMPTY,
+ "rotation": 0,
+ "powered": false,
+ "locked": false,
+ })
+ tiles.append(row)
+
+ # Load puzzle tiles
+ for tile_data in puzzle_data.get("tiles", []):
+ var x = tile_data.get("x", 0)
+ var y = tile_data.get("y", 0)
+ if x < grid_size.x and y < grid_size.y:
+ tiles[y][x] = {
+ "type": tile_data.get("type", TileType.EMPTY),
+ "rotation": tile_data.get("rotation", 0),
+ "powered": false,
+ "locked": tile_data.get("locked", false),
+ }
+
+func rotate_tile(x: int, y: int, clockwise: bool = true) -> bool:
+ if x < 0 or x >= grid_size.x or y < 0 or y >= grid_size.y:
+ return false
+
+ var tile = tiles[y][x]
+ if tile.locked or tile.type == TileType.EMPTY:
+ return false
+
+ # Save state for undo
+ undo_stack.append({"x": x, "y": y, "rotation": tile.rotation})
+
+ # Rotate
+ if clockwise:
+ tile.rotation = (tile.rotation + 1) % 4
+ else:
+ tile.rotation = (tile.rotation + 3) % 4
+
+ move_count += 1
+ move_made.emit(move_count)
+
+ # Update power flow
+ _update_power_flow()
+
+ return true
+
+func undo() -> bool:
+ if undo_stack.is_empty():
+ return false
+
+ var last_move = undo_stack.pop_back()
+ tiles[last_move.y][last_move.x].rotation = last_move.rotation
+ move_count = max(0, move_count - 1)
+ move_made.emit(move_count)
+
+ _update_power_flow()
+ return true
+
+func reset_puzzle() -> void:
+ load_puzzle(current_puzzle)
+ _update_power_flow()
+
+func get_tile_connections(tile: Dictionary) -> int:
+ var base = TILE_CONNECTIONS.get(tile.type, 0)
+ var rotation = tile.rotation
+
+ # Rotate connections
+ for i in rotation:
+ var new_connections = 0
+ if base & DIR_UP:
+ new_connections |= DIR_RIGHT
+ if base & DIR_RIGHT:
+ new_connections |= DIR_DOWN
+ if base & DIR_DOWN:
+ new_connections |= DIR_LEFT
+ if base & DIR_LEFT:
+ new_connections |= DIR_UP
+ base = new_connections
+
+ return base
+
+func _update_power_flow() -> void:
+ # Reset all power states
+ for y in grid_size.y:
+ for x in grid_size.x:
+ tiles[y][x].powered = false
+
+ # Find all sources and propagate power
+ var sources: Array[Vector2i] = []
+ for y in grid_size.y:
+ for x in grid_size.x:
+ if tiles[y][x].type == TileType.SOURCE:
+ sources.append(Vector2i(x, y))
+
+ # BFS from each source
+ for source in sources:
+ _propagate_power(source)
+
+ # Check win condition
+ _check_win_condition()
+
+func _propagate_power(start: Vector2i) -> void:
+ var visited: Array[Vector2i] = []
+ var queue: Array[Vector2i] = [start]
+
+ while not queue.is_empty():
+ var current = queue.pop_front()
+ if current in visited:
+ continue
+ visited.append(current)
+
+ var x = current.x
+ var y = current.y
+ var tile = tiles[y][x]
+ tile.powered = true
+ power_changed.emit(null, true) # Signal for UI update
+
+ var connections = get_tile_connections(tile)
+
+ # Check each direction
+ if connections & DIR_UP and y > 0:
+ var neighbor = tiles[y - 1][x]
+ if get_tile_connections(neighbor) & DIR_DOWN:
+ queue.append(Vector2i(x, y - 1))
+
+ if connections & DIR_DOWN and y < grid_size.y - 1:
+ var neighbor = tiles[y + 1][x]
+ if get_tile_connections(neighbor) & DIR_UP:
+ queue.append(Vector2i(x, y + 1))
+
+ if connections & DIR_LEFT and x > 0:
+ var neighbor = tiles[y][x - 1]
+ if get_tile_connections(neighbor) & DIR_RIGHT:
+ queue.append(Vector2i(x - 1, y))
+
+ if connections & DIR_RIGHT and x < grid_size.x - 1:
+ var neighbor = tiles[y][x + 1]
+ if get_tile_connections(neighbor) & DIR_LEFT:
+ queue.append(Vector2i(x + 1, y))
+
+func _check_win_condition() -> void:
+ for y in grid_size.y:
+ for x in grid_size.x:
+ var tile = tiles[y][x]
+ if tile.type == TileType.OUTPUT and not tile.powered:
+ return # Not all outputs powered
+
+ # All outputs are powered!
+ puzzle_solved.emit()
+
+func is_tile_powered(x: int, y: int) -> bool:
+ if x < 0 or x >= grid_size.x or y < 0 or y >= grid_size.y:
+ return false
+ return tiles[y][x].powered
diff --git a/showcase_games/circuit_logic/scripts/game/game_controller.gd b/showcase_games/circuit_logic/scripts/game/game_controller.gd
new file mode 100644
index 00000000..56ef197e
--- /dev/null
+++ b/showcase_games/circuit_logic/scripts/game/game_controller.gd
@@ -0,0 +1,310 @@
+extends Control
+## Game Controller for Circuit Logic
+
+@onready var grid_container: Control = $GridContainer
+@onready var level_name: Label = $UI/TopBar/LevelName
+@onready var moves_label: Label = $UI/TopBar/MovesLabel
+@onready var par_label: Label = $UI/BottomBar/ParLabel
+@onready var back_button: Button = $UI/TopBar/BackButton
+@onready var reset_button: Button = $UI/BottomBar/ResetButton
+@onready var undo_button: Button = $UI/BottomBar/UndoButton
+@onready var win_panel: PanelContainer = $WinPanel
+@onready var stars_label: Label = $WinPanel/VBox/StarsLabel
+@onready var moves_result: Label = $WinPanel/VBox/MovesResult
+@onready var next_button: Button = $WinPanel/VBox/NextButton
+@onready var menu_button: Button = $WinPanel/VBox/MenuButton
+
+const TILE_SIZE: float = 80.0
+const TILE_PADDING: float = 4.0
+
+var current_level_id: int = 1
+var tile_nodes: Array = [] # 2D array of tile Control nodes
+var start_time: float = 0.0
+var is_solved: bool = false
+
+# Colors
+const COLOR_EMPTY = Color(0.15, 0.17, 0.22, 1)
+const COLOR_UNPOWERED = Color(0.25, 0.28, 0.35, 1)
+const COLOR_POWERED = Color(0.3, 0.8, 0.5, 1)
+const COLOR_SOURCE = Color(1, 0.85, 0.3, 1)
+const COLOR_OUTPUT_OFF = Color(0.8, 0.3, 0.3, 1)
+const COLOR_OUTPUT_ON = Color(0.3, 0.8, 0.5, 1)
+const COLOR_WIRE = Color(0.4, 0.45, 0.55, 1)
+const COLOR_WIRE_POWERED = Color(0.4, 1, 0.6, 1)
+
+func _ready() -> void:
+ # Get level ID from scene tree or default to 1
+ current_level_id = get_tree().get_meta("current_level", 1)
+
+ # Connect buttons
+ back_button.pressed.connect(_on_back)
+ reset_button.pressed.connect(_on_reset)
+ undo_button.pressed.connect(_on_undo)
+ next_button.pressed.connect(_on_next)
+ menu_button.pressed.connect(_on_back)
+
+ # Connect puzzle signals
+ PuzzleManager.puzzle_solved.connect(_on_puzzle_solved)
+ PuzzleManager.move_made.connect(_on_move_made)
+
+ # Load and start level
+ _load_level(current_level_id)
+
+ # Fade in
+ modulate.a = 0.0
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 1.0, 0.3)
+
+func _load_level(level_id: int) -> void:
+ var level_data = ProgressManager.get_level(level_id)
+ if level_data.is_empty():
+ push_error("Level not found: ", level_id)
+ return
+
+ is_solved = false
+ win_panel.visible = false
+ start_time = Time.get_ticks_msec() / 1000.0
+
+ # Update UI
+ level_name.text = "Level %d: %s" % [level_data.id, level_data.name]
+ par_label.text = "Par: %d moves" % level_data.par_moves
+ moves_label.text = "Moves: 0"
+
+ # Load puzzle
+ PuzzleManager.load_puzzle(level_data)
+
+ # Build grid
+ _build_grid()
+
+ # Initial power update
+ PuzzleManager._update_power_flow()
+ _update_tile_visuals()
+
+func _build_grid() -> void:
+ # Clear existing tiles
+ for child in grid_container.get_children():
+ child.queue_free()
+ tile_nodes.clear()
+
+ var grid_size = PuzzleManager.grid_size
+ var total_width = grid_size.x * TILE_SIZE
+ var total_height = grid_size.y * TILE_SIZE
+
+ # Center the grid
+ grid_container.position = Vector2(
+ (get_viewport_rect().size.x - total_width) / 2,
+ (get_viewport_rect().size.y - total_height) / 2
+ )
+ grid_container.size = Vector2(total_width, total_height)
+
+ # Create tiles
+ for y in grid_size.y:
+ var row = []
+ for x in grid_size.x:
+ var tile_data = PuzzleManager.tiles[y][x]
+ var tile_node = _create_tile_node(x, y, tile_data)
+ grid_container.add_child(tile_node)
+ row.append(tile_node)
+ tile_nodes.append(row)
+
+func _create_tile_node(x: int, y: int, tile_data: Dictionary) -> Control:
+ var container = Control.new()
+ container.size = Vector2(TILE_SIZE, TILE_SIZE)
+ container.position = Vector2(x * TILE_SIZE, y * TILE_SIZE)
+
+ # Background
+ var bg = ColorRect.new()
+ bg.name = "Background"
+ bg.size = Vector2(TILE_SIZE - TILE_PADDING * 2, TILE_SIZE - TILE_PADDING * 2)
+ bg.position = Vector2(TILE_PADDING, TILE_PADDING)
+ bg.color = COLOR_EMPTY
+ container.add_child(bg)
+
+ # Wire container
+ var wire_container = Control.new()
+ wire_container.name = "Wires"
+ wire_container.size = bg.size
+ wire_container.position = bg.position
+ container.add_child(wire_container)
+
+ # Center node for source/output
+ var center = ColorRect.new()
+ center.name = "Center"
+ center.size = Vector2(20, 20)
+ center.position = Vector2(TILE_SIZE/2 - 10, TILE_SIZE/2 - 10)
+ center.visible = false
+ container.add_child(center)
+
+ # Click detection
+ var button = Button.new()
+ button.name = "ClickArea"
+ button.size = Vector2(TILE_SIZE, TILE_SIZE)
+ button.flat = true
+ button.modulate.a = 0.0
+ button.pressed.connect(_on_tile_clicked.bind(x, y))
+ button.mouse_entered.connect(_on_tile_hover.bind(container, true))
+ button.mouse_exited.connect(_on_tile_hover.bind(container, false))
+ container.add_child(button)
+
+ # Store position
+ container.set_meta("grid_x", x)
+ container.set_meta("grid_y", y)
+
+ return container
+
+func _update_tile_visuals() -> void:
+ for y in PuzzleManager.grid_size.y:
+ for x in PuzzleManager.grid_size.x:
+ var tile_data = PuzzleManager.tiles[y][x]
+ var tile_node = tile_nodes[y][x]
+ _draw_tile(tile_node, tile_data)
+
+func _draw_tile(tile_node: Control, tile_data: Dictionary) -> void:
+ var bg: ColorRect = tile_node.get_node("Background")
+ var wire_container: Control = tile_node.get_node("Wires")
+ var center: ColorRect = tile_node.get_node("Center")
+
+ # Clear wires
+ for child in wire_container.get_children():
+ child.queue_free()
+
+ # Set background color
+ if tile_data.type == PuzzleManager.TileType.EMPTY:
+ bg.color = COLOR_EMPTY
+ center.visible = false
+ return
+
+ bg.color = COLOR_UNPOWERED if not tile_data.powered else COLOR_POWERED.darkened(0.5)
+
+ # Get connections
+ var connections = PuzzleManager.get_tile_connections(tile_data)
+ var wire_color = COLOR_WIRE_POWERED if tile_data.powered else COLOR_WIRE
+ var tile_size = bg.size.x
+ var wire_width = 8.0
+ var center_size = tile_size * 0.25
+
+ # Draw wires
+ if connections & PuzzleManager.DIR_UP:
+ var wire = ColorRect.new()
+ wire.size = Vector2(wire_width, tile_size/2)
+ wire.position = Vector2(tile_size/2 - wire_width/2, 0)
+ wire.color = wire_color
+ wire_container.add_child(wire)
+
+ if connections & PuzzleManager.DIR_DOWN:
+ var wire = ColorRect.new()
+ wire.size = Vector2(wire_width, tile_size/2)
+ wire.position = Vector2(tile_size/2 - wire_width/2, tile_size/2)
+ wire.color = wire_color
+ wire_container.add_child(wire)
+
+ if connections & PuzzleManager.DIR_LEFT:
+ var wire = ColorRect.new()
+ wire.size = Vector2(tile_size/2, wire_width)
+ wire.position = Vector2(0, tile_size/2 - wire_width/2)
+ wire.color = wire_color
+ wire_container.add_child(wire)
+
+ if connections & PuzzleManager.DIR_RIGHT:
+ var wire = ColorRect.new()
+ wire.size = Vector2(tile_size/2, wire_width)
+ wire.position = Vector2(tile_size/2, tile_size/2 - wire_width/2)
+ wire.color = wire_color
+ wire_container.add_child(wire)
+
+ # Draw center node
+ center.visible = true
+ center.size = Vector2(center_size, center_size)
+ center.position = Vector2(TILE_SIZE/2 - center_size/2 - TILE_PADDING, TILE_SIZE/2 - center_size/2 - TILE_PADDING)
+
+ match tile_data.type:
+ PuzzleManager.TileType.SOURCE:
+ center.color = COLOR_SOURCE
+ PuzzleManager.TileType.OUTPUT:
+ center.color = COLOR_OUTPUT_ON if tile_data.powered else COLOR_OUTPUT_OFF
+ _:
+ center.color = wire_color
+ center.size = Vector2(wire_width * 1.5, wire_width * 1.5)
+ center.position = Vector2(TILE_SIZE/2 - center.size.x/2 - TILE_PADDING, TILE_SIZE/2 - center.size.y/2 - TILE_PADDING)
+
+func _on_tile_clicked(x: int, y: int) -> void:
+ if is_solved:
+ return
+
+ if PuzzleManager.rotate_tile(x, y, true):
+ _animate_tile_rotation(x, y)
+ _update_tile_visuals()
+
+func _animate_tile_rotation(x: int, y: int) -> void:
+ var tile_node = tile_nodes[y][x]
+ var tween = create_tween()
+ tween.tween_property(tile_node, "rotation", deg_to_rad(90), 0.15)
+ tween.tween_callback(func(): tile_node.rotation = 0)
+
+func _on_tile_hover(tile_node: Control, hovering: bool) -> void:
+ if is_solved:
+ return
+
+ var x = tile_node.get_meta("grid_x")
+ var y = tile_node.get_meta("grid_y")
+ var tile_data = PuzzleManager.tiles[y][x]
+
+ if tile_data.type == PuzzleManager.TileType.EMPTY or tile_data.locked:
+ return
+
+ var bg: ColorRect = tile_node.get_node("Background")
+ if hovering:
+ bg.modulate = Color(1.2, 1.2, 1.2, 1)
+ else:
+ bg.modulate = Color.WHITE
+
+func _on_move_made(moves: int) -> void:
+ moves_label.text = "Moves: %d" % moves
+
+func _on_puzzle_solved() -> void:
+ is_solved = true
+ var elapsed = (Time.get_ticks_msec() / 1000.0) - start_time
+ var stars = ProgressManager.complete_level(current_level_id, PuzzleManager.move_count, elapsed)
+
+ # Update win panel
+ var star_text = ""
+ for i in 3:
+ star_text += "★ " if i < stars else "☆ "
+ stars_label.text = star_text.strip_edges()
+ moves_result.text = "Completed in %d moves" % PuzzleManager.move_count
+
+ # Check if there's a next level
+ var next_level = ProgressManager.get_level(current_level_id + 1)
+ next_button.visible = not next_level.is_empty()
+
+ # Show win panel with animation
+ await get_tree().create_timer(0.5).timeout
+ win_panel.modulate.a = 0.0
+ win_panel.visible = true
+ var tween = create_tween()
+ tween.tween_property(win_panel, "modulate:a", 1.0, 0.3)
+
+func _on_back() -> void:
+ get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
+
+func _on_reset() -> void:
+ PuzzleManager.reset_puzzle()
+ _update_tile_visuals()
+ moves_label.text = "Moves: 0"
+ is_solved = false
+ win_panel.visible = false
+
+func _on_undo() -> void:
+ if PuzzleManager.undo():
+ _update_tile_visuals()
+
+func _on_next() -> void:
+ current_level_id += 1
+ get_tree().set_meta("current_level", current_level_id)
+ _load_level(current_level_id)
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed("reset_level"):
+ _on_reset()
+ elif event.is_action_pressed("undo"):
+ _on_undo()
diff --git a/showcase_games/circuit_logic/scripts/ui/level_select.gd b/showcase_games/circuit_logic/scripts/ui/level_select.gd
new file mode 100644
index 00000000..4ca0b61a
--- /dev/null
+++ b/showcase_games/circuit_logic/scripts/ui/level_select.gd
@@ -0,0 +1,112 @@
+extends Control
+## Level Select Screen
+
+@onready var back_button: Button = $VBox/Header/BackButton
+@onready var total_stars: Label = $VBox/Header/TotalStars
+@onready var chapter1_btn: Button = $VBox/ChapterTabs/Chapter1
+@onready var chapter2_btn: Button = $VBox/ChapterTabs/Chapter2
+@onready var level_grid: GridContainer = $VBox/ScrollContainer/LevelGrid
+
+var current_chapter: int = 1
+
+func _ready() -> void:
+ back_button.pressed.connect(_on_back)
+ chapter1_btn.pressed.connect(_on_chapter_selected.bind(1))
+ chapter2_btn.pressed.connect(_on_chapter_selected.bind(2))
+
+ _update_stars_display()
+ _update_chapter_buttons()
+ _populate_levels(1)
+
+ # Fade in
+ modulate.a = 0.0
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 1.0, 0.3)
+
+func _update_stars_display() -> void:
+ var stars = ProgressManager.get_total_stars()
+ var max_stars = ProgressManager.levels.size() * 3
+ total_stars.text = "★ %d / %d" % [stars, max_stars]
+
+func _update_chapter_buttons() -> void:
+ chapter1_btn.disabled = false
+ chapter2_btn.disabled = 2 not in ProgressManager.unlocked_chapters
+
+func _on_chapter_selected(chapter: int) -> void:
+ current_chapter = chapter
+ _populate_levels(chapter)
+
+func _populate_levels(chapter: int) -> void:
+ # Clear existing
+ for child in level_grid.get_children():
+ child.queue_free()
+
+ var levels = ProgressManager.get_levels_for_chapter(chapter)
+
+ for level in levels:
+ var card = _create_level_card(level)
+ level_grid.add_child(card)
+
+func _create_level_card(level: Dictionary) -> Control:
+ var card = PanelContainer.new()
+ card.custom_minimum_size = Vector2(180, 150)
+
+ var vbox = VBoxContainer.new()
+ vbox.add_theme_constant_override("separation", 10)
+ card.add_child(vbox)
+
+ # Level number
+ var num_label = Label.new()
+ num_label.text = str(level.id)
+ num_label.add_theme_font_size_override("font_size", 36)
+ num_label.add_theme_color_override("font_color", Color(0.3, 0.8, 0.5))
+ num_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ vbox.add_child(num_label)
+
+ # Level name
+ var name_label = Label.new()
+ name_label.text = level.name
+ name_label.add_theme_font_size_override("font_size", 16)
+ name_label.add_theme_color_override("font_color", Color(0.7, 0.8, 0.9))
+ name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
+ vbox.add_child(name_label)
+
+ # Stars
+ var stars_label = Label.new()
+ var progress = ProgressManager.completed_levels.get(level.id, {})
+ var stars = progress.get("stars", 0)
+ var star_text = ""
+ for i in 3:
+ star_text += "★" if i < stars else "☆"
+ stars_label.text = star_text
+ stars_label.add_theme_font_size_override("font_size", 24)
+ stars_label.add_theme_color_override("font_color", Color(1, 0.85, 0.3))
+ stars_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ vbox.add_child(stars_label)
+
+ # Play button
+ var play_btn = Button.new()
+ var unlocked = ProgressManager.is_level_unlocked(level.id)
+ play_btn.text = "PLAY" if unlocked else "🔒"
+ play_btn.disabled = not unlocked
+ play_btn.custom_minimum_size = Vector2(100, 35)
+ play_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
+ play_btn.pressed.connect(_on_level_selected.bind(level.id))
+ vbox.add_child(play_btn)
+
+ # Locked visual
+ if not unlocked:
+ card.modulate = Color(0.5, 0.5, 0.5, 1)
+
+ return card
+
+func _on_level_selected(level_id: int) -> void:
+ get_tree().set_meta("current_level", level_id)
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 0.0, 0.3)
+ await tween.finished
+ get_tree().change_scene_to_file("res://scenes/game.tscn")
+
+func _on_back() -> void:
+ get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
diff --git a/showcase_games/circuit_logic/scripts/ui/main_menu.gd b/showcase_games/circuit_logic/scripts/ui/main_menu.gd
new file mode 100644
index 00000000..4105611b
--- /dev/null
+++ b/showcase_games/circuit_logic/scripts/ui/main_menu.gd
@@ -0,0 +1,78 @@
+extends Control
+## Main Menu for Circuit Logic
+
+@onready var play_button: Button = $VBox/PlayButton
+@onready var level_select_button: Button = $VBox/LevelSelectButton
+@onready var quit_button: Button = $VBox/QuitButton
+@onready var stars_label: Label = $VBox/StarsLabel
+@onready var circuit_pattern: Control = $Background/CircuitPattern
+
+func _ready() -> void:
+ play_button.pressed.connect(_on_play)
+ level_select_button.pressed.connect(_on_level_select)
+ quit_button.pressed.connect(_on_quit)
+
+ # Update stars display
+ var total = ProgressManager.get_total_stars()
+ var max_stars = ProgressManager.levels.size() * 3
+ stars_label.text = "★ %d / %d" % [total, max_stars]
+
+ # Draw decorative circuit pattern
+ _draw_circuit_pattern()
+
+ # Fade in
+ modulate.a = 0.0
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 1.0, 0.5)
+
+func _draw_circuit_pattern() -> void:
+ # Create decorative background circuits
+ var viewport = get_viewport_rect().size
+
+ for i in 20:
+ var line = ColorRect.new()
+ line.color = Color(0.3, 0.8, 0.5, 0.1)
+
+ if randf() > 0.5:
+ # Horizontal line
+ line.size = Vector2(randf_range(50, 200), 2)
+ line.position = Vector2(randf() * viewport.x, randf() * viewport.y)
+ else:
+ # Vertical line
+ line.size = Vector2(2, randf_range(50, 200))
+ line.position = Vector2(randf() * viewport.x, randf() * viewport.y)
+
+ circuit_pattern.add_child(line)
+
+ # Add nodes at endpoints
+ var node_size = 6
+ var node = ColorRect.new()
+ node.color = Color(0.3, 0.8, 0.5, 0.15)
+ node.size = Vector2(node_size, node_size)
+ node.position = line.position - Vector2(node_size/2, node_size/2)
+ circuit_pattern.add_child(node)
+
+func _on_play() -> void:
+ # Find the first unlocked incomplete level, or first level
+ var next_level = 1
+ for level in ProgressManager.levels:
+ if ProgressManager.is_level_unlocked(level.id) and not ProgressManager.completed_levels.has(level.id):
+ next_level = level.id
+ break
+
+ _start_level(next_level)
+
+func _on_level_select() -> void:
+ get_tree().change_scene_to_file("res://scenes/level_select.tscn")
+
+func _start_level(level_id: int) -> void:
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 0.0, 0.3)
+ await tween.finished
+
+ # Store level to load
+ get_tree().set_meta("current_level", level_id)
+ get_tree().change_scene_to_file("res://scenes/game.tscn")
+
+func _on_quit() -> void:
+ get_tree().quit()
diff --git a/showcase_games/neon_runner/icon.svg b/showcase_games/neon_runner/icon.svg
new file mode 100644
index 00000000..b190dcb0
--- /dev/null
+++ b/showcase_games/neon_runner/icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/showcase_games/neon_runner/project.godot b/showcase_games/neon_runner/project.godot
new file mode 100644
index 00000000..b217c7b4
--- /dev/null
+++ b/showcase_games/neon_runner/project.godot
@@ -0,0 +1,61 @@
+; 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="Neon Runner"
+config/description="A synthwave-themed endless runner. Dash through neon-lit cityscapes, dodge obstacles, and chase high scores."
+run/main_scene="res://scenes/main_menu.tscn"
+config/features=PackedStringArray("4.7", "Forward Plus")
+config/icon="res://icon.svg"
+
+[autoload]
+
+GameManager="*res://scripts/autoload/game_manager.gd"
+AudioManager="*res://scripts/autoload/audio_manager.gd"
+
+[display]
+
+window/size/viewport_width=1920
+window/size/viewport_height=1080
+window/stretch/mode="canvas_items"
+
+[input]
+
+jump={
+"deadzone": 0.5,
+"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)
+, 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)
+]
+}
+slide={
+"deadzone": 0.5,
+"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)
+]
+}
+pause={
+"deadzone": 0.5,
+"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":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+
+[layer_names]
+
+2d_physics/layer_1="player"
+2d_physics/layer_2="obstacles"
+2d_physics/layer_3="collectibles"
+2d_physics/layer_4="ground"
+
+[rendering]
+
+renderer/rendering_method="forward_plus"
+environment/defaults/default_clear_color=Color(0.05, 0.02, 0.1, 1)
diff --git a/showcase_games/neon_runner/scenes/game.tscn b/showcase_games/neon_runner/scenes/game.tscn
new file mode 100644
index 00000000..e8af8bcb
--- /dev/null
+++ b/showcase_games/neon_runner/scenes/game.tscn
@@ -0,0 +1,222 @@
+[gd_scene load_steps=5 format=3 uid="uid://game_scene"]
+
+[ext_resource type="Script" path="res://scripts/game/game_controller.gd" id="1_game"]
+[ext_resource type="PackedScene" uid="uid://player_scene" path="res://scenes/player.tscn" id="2_player"]
+[ext_resource type="Script" path="res://scripts/game/level_generator.gd" id="3_gen"]
+[ext_resource type="Script" path="res://scripts/ui/game_ui.gd" id="4_ui"]
+
+[node name="Game" type="Node2D"]
+script = ExtResource("1_game")
+
+[node name="ParallaxBackground" type="ParallaxBackground" parent="."]
+
+[node name="Sky" type="ParallaxLayer" parent="ParallaxBackground"]
+motion_scale = Vector2(0.1, 0.1)
+
+[node name="ColorRect" type="ColorRect" parent="ParallaxBackground/Sky"]
+offset_left = -100.0
+offset_top = -100.0
+offset_right = 2020.0
+offset_bottom = 1180.0
+color = Color(0.05, 0.02, 0.15, 1)
+
+[node name="Buildings" type="ParallaxLayer" parent="ParallaxBackground"]
+motion_scale = Vector2(0.3, 0.3)
+
+[node name="FarBuildings" type="ParallaxLayer" parent="ParallaxBackground"]
+motion_scale = Vector2(0.5, 0.5)
+
+[node name="Ground" type="StaticBody2D" parent="."]
+position = Vector2(960, 900)
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Ground"]
+shape = SubResource("RectangleShape2D_ground")
+
+[node name="GroundVisual" type="ColorRect" parent="Ground"]
+offset_left = -1000.0
+offset_top = -10.0
+offset_right = 1000.0
+offset_bottom = 200.0
+color = Color(0.1, 0.05, 0.2, 1)
+
+[node name="GroundLine" type="ColorRect" parent="Ground"]
+offset_left = -1000.0
+offset_top = -12.0
+offset_right = 1000.0
+offset_bottom = -8.0
+color = Color(1, 0.2, 0.6, 1)
+
+[node name="Player" parent="." instance=ExtResource("2_player")]
+position = Vector2(300, 800)
+
+[node name="LevelGenerator" type="Node2D" parent="."]
+script = ExtResource("3_gen")
+
+[node name="ObstacleContainer" type="Node2D" parent="LevelGenerator"]
+
+[node name="CollectibleContainer" type="Node2D" parent="LevelGenerator"]
+
+[node name="Camera2D" type="Camera2D" parent="."]
+position = Vector2(960, 540)
+
+[node name="CanvasLayer" type="CanvasLayer" parent="."]
+
+[node name="GameUI" type="Control" parent="CanvasLayer"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("4_ui")
+
+[node name="ScoreLabel" type="Label" parent="CanvasLayer/GameUI"]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -200.0
+offset_top = 20.0
+offset_right = -20.0
+offset_bottom = 60.0
+grow_horizontal = 0
+theme_override_colors/font_color = Color(0, 1, 1, 1)
+theme_override_font_sizes/font_size = 32
+text = "0"
+horizontal_alignment = 2
+
+[node name="MultiplierLabel" type="Label" parent="CanvasLayer/GameUI"]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -200.0
+offset_top = 60.0
+offset_right = -20.0
+offset_bottom = 90.0
+grow_horizontal = 0
+theme_override_colors/font_color = Color(1, 0.8, 0, 1)
+theme_override_font_sizes/font_size = 20
+text = "x1.0"
+horizontal_alignment = 2
+
+[node name="PauseMenu" type="Control" parent="CanvasLayer/GameUI"]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="Overlay" type="ColorRect" parent="CanvasLayer/GameUI/PauseMenu"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0, 0, 0, 0.7)
+
+[node name="VBox" type="VBoxContainer" parent="CanvasLayer/GameUI/PauseMenu"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -150.0
+offset_top = -100.0
+offset_right = 150.0
+offset_bottom = 100.0
+theme_override_constants/separation = 20
+
+[node name="PausedLabel" type="Label" parent="CanvasLayer/GameUI/PauseMenu/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.2, 0.6, 1)
+theme_override_font_sizes/font_size = 48
+text = "PAUSED"
+horizontal_alignment = 1
+
+[node name="ResumeButton" type="Button" parent="CanvasLayer/GameUI/PauseMenu/VBox"]
+custom_minimum_size = Vector2(200, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "RESUME"
+
+[node name="RestartButton" type="Button" parent="CanvasLayer/GameUI/PauseMenu/VBox"]
+custom_minimum_size = Vector2(200, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "RESTART"
+
+[node name="MenuButton" type="Button" parent="CanvasLayer/GameUI/PauseMenu/VBox"]
+custom_minimum_size = Vector2(200, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "MAIN MENU"
+
+[node name="GameOverMenu" type="Control" parent="CanvasLayer/GameUI"]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="Overlay" type="ColorRect" parent="CanvasLayer/GameUI/GameOverMenu"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.1, 0, 0.05, 0.9)
+
+[node name="VBox" type="VBoxContainer" parent="CanvasLayer/GameUI/GameOverMenu"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -150.0
+offset_right = 200.0
+offset_bottom = 150.0
+theme_override_constants/separation = 15
+
+[node name="GameOverLabel" type="Label" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.2, 0.6, 1)
+theme_override_font_sizes/font_size = 56
+text = "GAME OVER"
+horizontal_alignment = 1
+
+[node name="FinalScoreLabel" type="Label" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 1, 1, 1)
+theme_override_font_sizes/font_size = 32
+text = "SCORE: 0"
+horizontal_alignment = 1
+
+[node name="HighScoreLabel" type="Label" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.8, 0, 1)
+theme_override_font_sizes/font_size = 24
+text = "HIGH SCORE: 0"
+horizontal_alignment = 1
+
+[node name="NewHighScore" type="Label" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+visible = false
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 1, 0, 1)
+theme_override_font_sizes/font_size = 20
+text = "★ NEW HIGH SCORE! ★"
+horizontal_alignment = 1
+
+[node name="RetryButton" type="Button" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+custom_minimum_size = Vector2(200, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "RETRY"
+
+[node name="MenuButton" type="Button" parent="CanvasLayer/GameUI/GameOverMenu/VBox"]
+custom_minimum_size = Vector2(200, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "MAIN MENU"
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_ground"]
+size = Vector2(4000, 20)
diff --git a/showcase_games/neon_runner/scenes/main_menu.tscn b/showcase_games/neon_runner/scenes/main_menu.tscn
new file mode 100644
index 00000000..00d4aa3a
--- /dev/null
+++ b/showcase_games/neon_runner/scenes/main_menu.tscn
@@ -0,0 +1,100 @@
+[gd_scene load_steps=2 format=3 uid="uid://main_menu_scene"]
+
+[ext_resource type="Script" path="res://scripts/ui/main_menu.gd" id="1_menu"]
+
+[node name="MainMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_menu")
+
+[node name="Background" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.05, 0.02, 0.1, 1)
+
+[node name="GridLines" type="ColorRect" parent="Background"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -150.0
+offset_right = 200.0
+offset_bottom = 150.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 20
+
+[node name="Title" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.2, 0.6, 1)
+theme_override_colors/font_outline_color = Color(0, 1, 1, 1)
+theme_override_constants/outline_size = 4
+theme_override_font_sizes/font_size = 72
+text = "NEON RUNNER"
+horizontal_alignment = 1
+
+[node name="Subtitle" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 1, 1, 0.8)
+theme_override_font_sizes/font_size = 18
+text = "Dash through the neon city"
+horizontal_alignment = 1
+
+[node name="Spacer" type="Control" parent="VBox"]
+custom_minimum_size = Vector2(0, 40)
+layout_mode = 2
+
+[node name="PlayButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(300, 60)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_colors/font_hover_color = Color(0.05, 0.02, 0.1, 1)
+theme_override_font_sizes/font_size = 28
+text = "▶ PLAY"
+
+[node name="HighScoreLabel" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.8, 0, 1)
+theme_override_font_sizes/font_size = 24
+text = "HIGH SCORE: 0"
+horizontal_alignment = 1
+
+[node name="QuitButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(200, 40)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 18
+text = "QUIT"
+
+[node name="Credits" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -150.0
+offset_top = -40.0
+offset_right = 150.0
+grow_horizontal = 2
+grow_vertical = 0
+theme_override_colors/font_color = Color(1, 1, 1, 0.5)
+theme_override_font_sizes/font_size = 14
+text = "Made with AeThex Engine"
+horizontal_alignment = 1
diff --git a/showcase_games/neon_runner/scenes/player.tscn b/showcase_games/neon_runner/scenes/player.tscn
new file mode 100644
index 00000000..cdf2533e
--- /dev/null
+++ b/showcase_games/neon_runner/scenes/player.tscn
@@ -0,0 +1,79 @@
+[gd_scene load_steps=3 format=3 uid="uid://player_scene"]
+
+[ext_resource type="Script" path="res://scripts/game/player.gd" id="1_player"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_player"]
+size = Vector2(40, 80)
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_slide"]
+size = Vector2(60, 30)
+
+[node name="Player" type="CharacterBody2D"]
+collision_layer = 1
+collision_mask = 6
+script = ExtResource("1_player")
+
+[node name="Sprite" type="ColorRect" parent="."]
+offset_left = -20.0
+offset_top = -80.0
+offset_right = 20.0
+color = Color(0, 1, 1, 1)
+
+[node name="GlowEffect" type="ColorRect" parent="Sprite"]
+modulate = Color(1, 1, 1, 0.3)
+offset_left = -5.0
+offset_top = -5.0
+offset_right = 45.0
+offset_bottom = 85.0
+color = Color(0, 1, 1, 1)
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+position = Vector2(0, -40)
+shape = SubResource("RectangleShape2D_player")
+
+[node name="SlideCollision" type="CollisionShape2D" parent="."]
+visible = false
+position = Vector2(0, -15)
+disabled = true
+shape = SubResource("RectangleShape2D_slide")
+
+[node name="TrailParticles" type="GPUParticles2D" parent="."]
+position = Vector2(-20, 0)
+emitting = false
+amount = 20
+process_material = SubResource("ParticlesMaterial_trail")
+lifetime = 0.5
+
+[node name="JumpParticles" type="GPUParticles2D" parent="."]
+emitting = false
+amount = 15
+one_shot = true
+explosiveness = 1.0
+process_material = SubResource("ParticlesMaterial_jump")
+lifetime = 0.3
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
+
+[sub_resource type="ParticleProcessMaterial" id="ParticlesMaterial_trail"]
+emission_shape = 1
+emission_sphere_radius = 5.0
+direction = Vector3(-1, 0, 0)
+spread = 15.0
+initial_velocity_min = 50.0
+initial_velocity_max = 100.0
+gravity = Vector3(0, 0, 0)
+scale_min = 0.5
+scale_max = 1.5
+color = Color(0, 1, 1, 0.8)
+
+[sub_resource type="ParticleProcessMaterial" id="ParticlesMaterial_jump"]
+emission_shape = 1
+emission_sphere_radius = 20.0
+direction = Vector3(0, 1, 0)
+spread = 60.0
+initial_velocity_min = 100.0
+initial_velocity_max = 200.0
+gravity = Vector3(0, 300, 0)
+scale_min = 0.3
+scale_max = 1.0
+color = Color(1, 0.2, 0.6, 1)
diff --git a/showcase_games/neon_runner/scripts/autoload/audio_manager.gd b/showcase_games/neon_runner/scripts/autoload/audio_manager.gd
new file mode 100644
index 00000000..4e6d3570
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/autoload/audio_manager.gd
@@ -0,0 +1,66 @@
+extends Node
+## Audio Manager - Handles all game audio with synthwave vibes
+
+var music_player: AudioStreamPlayer
+var sfx_players: Array[AudioStreamPlayer] = []
+var current_sfx_index: int = 0
+const MAX_SFX_PLAYERS = 8
+
+var music_volume: float = 0.8:
+ set(value):
+ music_volume = clamp(value, 0.0, 1.0)
+ if music_player:
+ music_player.volume_db = linear_to_db(music_volume)
+
+var sfx_volume: float = 1.0:
+ set(value):
+ sfx_volume = clamp(value, 0.0, 1.0)
+
+func _ready() -> void:
+ # Create music player
+ music_player = AudioStreamPlayer.new()
+ music_player.bus = "Music"
+ add_child(music_player)
+
+ # Create SFX player pool
+ for i in MAX_SFX_PLAYERS:
+ var player = AudioStreamPlayer.new()
+ player.bus = "SFX"
+ add_child(player)
+ sfx_players.append(player)
+
+func play_music(stream: AudioStream, fade_in: float = 1.0) -> void:
+ if music_player.playing:
+ var tween = create_tween()
+ tween.tween_property(music_player, "volume_db", -40.0, 0.5)
+ await tween.finished
+
+ music_player.stream = stream
+ music_player.volume_db = -40.0
+ music_player.play()
+
+ var tween = create_tween()
+ tween.tween_property(music_player, "volume_db", linear_to_db(music_volume), fade_in)
+
+func stop_music(fade_out: float = 1.0) -> void:
+ if music_player.playing:
+ var tween = create_tween()
+ tween.tween_property(music_player, "volume_db", -40.0, fade_out)
+ await tween.finished
+ music_player.stop()
+
+func play_sfx(stream: AudioStream, pitch_variance: float = 0.0) -> void:
+ var player = sfx_players[current_sfx_index]
+ current_sfx_index = (current_sfx_index + 1) % MAX_SFX_PLAYERS
+
+ player.stream = stream
+ player.volume_db = linear_to_db(sfx_volume)
+ if pitch_variance > 0:
+ player.pitch_scale = randf_range(1.0 - pitch_variance, 1.0 + pitch_variance)
+ else:
+ player.pitch_scale = 1.0
+ player.play()
+
+func play_sfx_at_position(stream: AudioStream, _position: Vector2) -> void:
+ # For 2D positional audio - simplified for this game
+ play_sfx(stream)
diff --git a/showcase_games/neon_runner/scripts/autoload/game_manager.gd b/showcase_games/neon_runner/scripts/autoload/game_manager.gd
new file mode 100644
index 00000000..7df51ef8
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/autoload/game_manager.gd
@@ -0,0 +1,89 @@
+extends Node
+## Game Manager - Global game state and scoring
+
+signal score_changed(new_score: int)
+signal high_score_changed(new_high_score: int)
+signal game_over
+signal game_started
+signal multiplier_changed(multiplier: float)
+
+var score: int = 0:
+ set(value):
+ score = value
+ score_changed.emit(score)
+ if score > high_score:
+ high_score = score
+
+var high_score: int = 0:
+ set(value):
+ high_score = value
+ high_score_changed.emit(high_score)
+ _save_high_score()
+
+var distance: float = 0.0
+var multiplier: float = 1.0:
+ set(value):
+ multiplier = value
+ multiplier_changed.emit(multiplier)
+
+var game_speed: float = 400.0
+var base_speed: float = 400.0
+var max_speed: float = 1200.0
+var speed_increase_rate: float = 0.5
+
+var is_playing: bool = false
+var is_paused: bool = false
+
+const SAVE_PATH = "user://neon_runner_save.dat"
+
+func _ready() -> void:
+ _load_high_score()
+
+func _process(delta: float) -> void:
+ if is_playing and not is_paused:
+ distance += game_speed * delta
+ # Gradually increase speed
+ game_speed = min(game_speed + speed_increase_rate * delta, max_speed)
+ # Add distance-based score
+ score += int(game_speed * delta * 0.01 * multiplier)
+
+func start_game() -> void:
+ score = 0
+ distance = 0.0
+ multiplier = 1.0
+ game_speed = base_speed
+ is_playing = true
+ is_paused = false
+ game_started.emit()
+
+func end_game() -> void:
+ is_playing = false
+ game_over.emit()
+
+func pause_game() -> void:
+ is_paused = true
+ get_tree().paused = true
+
+func resume_game() -> void:
+ is_paused = false
+ get_tree().paused = false
+
+func add_score(amount: int) -> void:
+ score += int(amount * multiplier)
+
+func add_multiplier(amount: float) -> void:
+ multiplier = min(multiplier + amount, 5.0)
+
+func reset_multiplier() -> void:
+ multiplier = 1.0
+
+func _save_high_score() -> void:
+ var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
+ if file:
+ file.store_var(high_score)
+
+func _load_high_score() -> void:
+ if FileAccess.file_exists(SAVE_PATH):
+ var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if file:
+ high_score = file.get_var()
diff --git a/showcase_games/neon_runner/scripts/game/game_controller.gd b/showcase_games/neon_runner/scripts/game/game_controller.gd
new file mode 100644
index 00000000..c6482fe9
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/game/game_controller.gd
@@ -0,0 +1,33 @@
+extends Node2D
+## Game Controller - Main game loop and coordination
+
+@onready var player: Player = $Player
+@onready var level_generator: Node2D = $LevelGenerator
+@onready var game_ui: Control = $CanvasLayer/GameUI
+
+func _ready() -> void:
+ # Connect player signals
+ player.died.connect(_on_player_died)
+
+ # Start the game
+ await get_tree().create_timer(0.5).timeout
+ GameManager.start_game()
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed("pause") and GameManager.is_playing:
+ if GameManager.is_paused:
+ GameManager.resume_game()
+ else:
+ GameManager.pause_game()
+
+func _on_player_died() -> void:
+ # Game over is handled by GameManager
+ pass
+
+func restart_game() -> void:
+ GameManager.start_game()
+ level_generator.reset()
+
+func return_to_menu() -> void:
+ GameManager.is_playing = false
+ get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
diff --git a/showcase_games/neon_runner/scripts/game/level_generator.gd b/showcase_games/neon_runner/scripts/game/level_generator.gd
new file mode 100644
index 00000000..5b3726b8
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/game/level_generator.gd
@@ -0,0 +1,272 @@
+extends Node2D
+## Level Generator - Procedural obstacle and collectible spawning
+
+@onready var obstacle_container: Node2D = $ObstacleContainer
+@onready var collectible_container: Node2D = $CollectibleContainer
+
+const SPAWN_X: float = 2100.0 # Off-screen right
+const DESPAWN_X: float = -200.0 # Off-screen left
+
+var spawn_timer: float = 0.0
+var min_spawn_interval: float = 0.8
+var max_spawn_interval: float = 2.0
+var current_spawn_interval: float = 1.5
+
+var obstacle_scene: PackedScene
+var collectible_scene: PackedScene
+
+# Obstacle patterns
+enum ObstacleType { LOW, HIGH, DOUBLE, MOVING }
+var last_obstacle_type: ObstacleType = ObstacleType.LOW
+
+func _ready() -> void:
+ # Create obstacle and collectible scenes programmatically
+ _create_obstacle_scene()
+ _create_collectible_scene()
+
+ GameManager.game_started.connect(_on_game_started)
+
+func _process(delta: float) -> void:
+ if not GameManager.is_playing or GameManager.is_paused:
+ return
+
+ # Move all obstacles and collectibles
+ var speed = GameManager.game_speed * delta
+
+ for obstacle in obstacle_container.get_children():
+ obstacle.position.x -= speed
+ if obstacle.position.x < DESPAWN_X:
+ obstacle.queue_free()
+
+ for collectible in collectible_container.get_children():
+ collectible.position.x -= speed
+ if collectible.position.x < DESPAWN_X:
+ collectible.queue_free()
+
+ # Spawn new obstacles
+ spawn_timer -= delta
+ if spawn_timer <= 0:
+ _spawn_obstacle()
+ _maybe_spawn_collectibles()
+ spawn_timer = randf_range(min_spawn_interval, current_spawn_interval)
+
+ # Increase difficulty over time
+ current_spawn_interval = max(min_spawn_interval,
+ current_spawn_interval - 0.01)
+
+func _spawn_obstacle() -> void:
+ var obstacle = _create_obstacle()
+ obstacle_container.add_child(obstacle)
+
+func _create_obstacle() -> Node2D:
+ var obstacle = Area2D.new()
+ obstacle.collision_layer = 2
+ obstacle.collision_mask = 1
+ obstacle.position = Vector2(SPAWN_X, 0)
+
+ # Choose obstacle type (avoid repeating)
+ var obstacle_type = _choose_obstacle_type()
+ last_obstacle_type = obstacle_type
+
+ match obstacle_type:
+ ObstacleType.LOW:
+ _setup_low_obstacle(obstacle)
+ ObstacleType.HIGH:
+ _setup_high_obstacle(obstacle)
+ ObstacleType.DOUBLE:
+ _setup_double_obstacle(obstacle)
+ ObstacleType.MOVING:
+ _setup_moving_obstacle(obstacle)
+
+ obstacle.body_entered.connect(_on_obstacle_hit)
+
+ return obstacle
+
+func _choose_obstacle_type() -> ObstacleType:
+ var types = [ObstacleType.LOW, ObstacleType.HIGH, ObstacleType.DOUBLE]
+
+ # Add moving obstacles once player has progressed
+ if GameManager.distance > 5000:
+ types.append(ObstacleType.MOVING)
+
+ # Remove last type to add variety
+ types.erase(last_obstacle_type)
+
+ return types[randi() % types.size()]
+
+func _setup_low_obstacle(obstacle: Area2D) -> void:
+ # Ground-level obstacle - player must jump
+ obstacle.position.y = 850
+
+ var shape = CollisionShape2D.new()
+ var rect = RectangleShape2D.new()
+ rect.size = Vector2(60, 80)
+ shape.shape = rect
+ shape.position = Vector2(0, -40)
+ obstacle.add_child(shape)
+
+ var visual = ColorRect.new()
+ visual.size = Vector2(60, 80)
+ visual.position = Vector2(-30, -80)
+ visual.color = Color(1, 0.2, 0.6, 1)
+ obstacle.add_child(visual)
+
+ # Glow effect
+ var glow = ColorRect.new()
+ glow.size = Vector2(70, 90)
+ glow.position = Vector2(-35, -85)
+ glow.color = Color(1, 0.2, 0.6, 0.3)
+ obstacle.add_child(glow)
+ glow.z_index = -1
+
+func _setup_high_obstacle(obstacle: Area2D) -> void:
+ # Floating obstacle - player must slide under
+ obstacle.position.y = 750
+
+ var shape = CollisionShape2D.new()
+ var rect = RectangleShape2D.new()
+ rect.size = Vector2(100, 40)
+ shape.shape = rect
+ obstacle.add_child(shape)
+
+ var visual = ColorRect.new()
+ visual.size = Vector2(100, 40)
+ visual.position = Vector2(-50, -20)
+ visual.color = Color(1, 0.5, 0, 1)
+ obstacle.add_child(visual)
+
+func _setup_double_obstacle(obstacle: Area2D) -> void:
+ # Two obstacles with a gap - requires precise timing
+ obstacle.position.y = 850
+
+ # First obstacle (low)
+ var shape1 = CollisionShape2D.new()
+ var rect1 = RectangleShape2D.new()
+ rect1.size = Vector2(40, 60)
+ shape1.shape = rect1
+ shape1.position = Vector2(-60, -30)
+ obstacle.add_child(shape1)
+
+ var visual1 = ColorRect.new()
+ visual1.size = Vector2(40, 60)
+ visual1.position = Vector2(-80, -60)
+ visual1.color = Color(1, 0.2, 0.6, 1)
+ obstacle.add_child(visual1)
+
+ # Second obstacle (low)
+ var shape2 = CollisionShape2D.new()
+ var rect2 = RectangleShape2D.new()
+ rect2.size = Vector2(40, 60)
+ shape2.shape = rect2
+ shape2.position = Vector2(60, -30)
+ obstacle.add_child(shape2)
+
+ var visual2 = ColorRect.new()
+ visual2.size = Vector2(40, 60)
+ visual2.position = Vector2(40, -60)
+ visual2.color = Color(1, 0.2, 0.6, 1)
+ obstacle.add_child(visual2)
+
+func _setup_moving_obstacle(obstacle: Area2D) -> void:
+ # Obstacle that moves up and down
+ obstacle.position.y = 800
+
+ var shape = CollisionShape2D.new()
+ var rect = RectangleShape2D.new()
+ rect.size = Vector2(50, 50)
+ shape.shape = rect
+ obstacle.add_child(shape)
+
+ var visual = ColorRect.new()
+ visual.size = Vector2(50, 50)
+ visual.position = Vector2(-25, -25)
+ visual.color = Color(0.8, 0, 1, 1)
+ obstacle.add_child(visual)
+
+ # Add movement animation
+ var tween = create_tween().set_loops()
+ tween.tween_property(obstacle, "position:y", 700, 0.5)
+ tween.tween_property(obstacle, "position:y", 850, 0.5)
+
+func _maybe_spawn_collectibles() -> void:
+ if randf() > 0.6: # 40% chance
+ return
+
+ var count = randi_range(3, 7)
+ var pattern = randi() % 3 # 0: line, 1: arc, 2: zigzag
+
+ for i in count:
+ var collectible = _create_collectible()
+
+ match pattern:
+ 0: # Line
+ collectible.position = Vector2(SPAWN_X + i * 80, 700)
+ 1: # Arc
+ var arc_y = 750 - sin(i * PI / count) * 150
+ collectible.position = Vector2(SPAWN_X + i * 80, arc_y)
+ 2: # Zigzag
+ var zig_y = 750 if i % 2 == 0 else 650
+ collectible.position = Vector2(SPAWN_X + i * 60, zig_y)
+
+ collectible_container.add_child(collectible)
+
+func _create_collectible() -> Area2D:
+ var collectible = Area2D.new()
+ collectible.collision_layer = 4
+ collectible.collision_mask = 1
+
+ var shape = CollisionShape2D.new()
+ var circle = CircleShape2D.new()
+ circle.radius = 15
+ shape.shape = circle
+ collectible.add_child(shape)
+
+ var visual = ColorRect.new()
+ visual.size = Vector2(30, 30)
+ visual.position = Vector2(-15, -15)
+ visual.color = Color(1, 0.8, 0, 1)
+ collectible.add_child(visual)
+
+ # Rotation animation
+ var tween = create_tween().set_loops()
+ tween.tween_property(visual, "rotation", TAU, 1.0)
+
+ collectible.body_entered.connect(_on_collectible_collected.bind(collectible))
+
+ return collectible
+
+func _on_obstacle_hit(body: Node2D) -> void:
+ if body is Player:
+ body.hit_obstacle()
+
+func _on_collectible_collected(body: Node2D, collectible: Area2D) -> void:
+ if body is Player:
+ body.collect_item(100, 0.1)
+
+ # Collection effect
+ var tween = create_tween()
+ tween.tween_property(collectible, "scale", Vector2(1.5, 1.5), 0.1)
+ tween.parallel().tween_property(collectible, "modulate:a", 0.0, 0.1)
+ await tween.finished
+ collectible.queue_free()
+
+func reset() -> void:
+ # Clear all obstacles and collectibles
+ for child in obstacle_container.get_children():
+ child.queue_free()
+ for child in collectible_container.get_children():
+ child.queue_free()
+
+ spawn_timer = 1.0
+ current_spawn_interval = max_spawn_interval
+
+func _on_game_started() -> void:
+ reset()
+
+func _create_obstacle_scene() -> void:
+ # Scenes created programmatically in _create_obstacle()
+ pass
+
+func _create_collectible_scene() -> void:
+ # Scenes created programmatically in _create_collectible()
+ pass
diff --git a/showcase_games/neon_runner/scripts/game/player.gd b/showcase_games/neon_runner/scripts/game/player.gd
new file mode 100644
index 00000000..3e7c6f78
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/game/player.gd
@@ -0,0 +1,149 @@
+extends CharacterBody2D
+class_name Player
+## Neon Runner Player Controller
+
+signal died
+signal jumped
+signal landed
+signal started_slide
+signal ended_slide
+
+const GRAVITY: float = 2000.0
+const JUMP_VELOCITY: float = -800.0
+const DOUBLE_JUMP_VELOCITY: float = -600.0
+const MAX_JUMPS: int = 2
+
+@onready var sprite: ColorRect = $Sprite
+@onready var collision_standing: CollisionShape2D = $CollisionShape2D
+@onready var collision_sliding: CollisionShape2D = $SlideCollision
+@onready var trail_particles: GPUParticles2D = $TrailParticles
+@onready var jump_particles: GPUParticles2D = $JumpParticles
+
+var jumps_remaining: int = MAX_JUMPS
+var is_sliding: bool = false
+var is_dead: bool = false
+var original_sprite_size: Vector2
+
+func _ready() -> void:
+ original_sprite_size = sprite.size
+ GameManager.game_started.connect(_on_game_started)
+ GameManager.game_over.connect(_on_game_over)
+
+func _physics_process(delta: float) -> void:
+ if is_dead:
+ return
+
+ if not GameManager.is_playing:
+ return
+
+ # Apply gravity
+ velocity.y += GRAVITY * delta
+
+ # Handle jump input
+ if Input.is_action_just_pressed("jump") and jumps_remaining > 0:
+ _jump()
+
+ # Handle slide input
+ if Input.is_action_pressed("slide") and is_on_floor() and not is_sliding:
+ _start_slide()
+ elif Input.is_action_just_released("slide") and is_sliding:
+ _end_slide()
+
+ # Auto end slide if in air
+ if is_sliding and not is_on_floor():
+ _end_slide()
+
+ move_and_slide()
+
+ # Check if landed
+ if is_on_floor() and velocity.y >= 0:
+ if jumps_remaining < MAX_JUMPS:
+ landed.emit()
+ jumps_remaining = MAX_JUMPS
+
+ # Update trail particles
+ trail_particles.emitting = GameManager.is_playing and not is_dead
+
+func _jump() -> void:
+ var jump_power = JUMP_VELOCITY if jumps_remaining == MAX_JUMPS else DOUBLE_JUMP_VELOCITY
+ velocity.y = jump_power
+ jumps_remaining -= 1
+ jumped.emit()
+
+ # Visual feedback
+ jump_particles.restart()
+ jump_particles.emitting = true
+
+ # Squash and stretch
+ var tween = create_tween()
+ tween.tween_property(sprite, "scale", Vector2(0.8, 1.3), 0.1)
+ tween.tween_property(sprite, "scale", Vector2(1.0, 1.0), 0.2)
+
+ if is_sliding:
+ _end_slide()
+
+func _start_slide() -> void:
+ is_sliding = true
+ collision_standing.disabled = true
+ collision_sliding.disabled = false
+ collision_sliding.visible = true
+
+ # Squash sprite for slide
+ var tween = create_tween()
+ tween.tween_property(sprite, "scale", Vector2(1.5, 0.4), 0.1)
+ tween.parallel().tween_property(sprite, "position:y", -20, 0.1)
+
+ started_slide.emit()
+
+func _end_slide() -> void:
+ is_sliding = false
+ collision_standing.disabled = false
+ collision_sliding.disabled = true
+ collision_sliding.visible = false
+
+ # Return sprite to normal
+ var tween = create_tween()
+ tween.tween_property(sprite, "scale", Vector2(1.0, 1.0), 0.15)
+ tween.parallel().tween_property(sprite, "position:y", 0, 0.15)
+
+ ended_slide.emit()
+
+func hit_obstacle() -> void:
+ if is_dead:
+ return
+
+ is_dead = true
+ trail_particles.emitting = false
+
+ # Death animation
+ var tween = create_tween()
+ tween.tween_property(sprite, "modulate", Color(1, 0, 0, 1), 0.1)
+ tween.tween_property(self, "rotation", PI * 2, 0.5)
+ tween.parallel().tween_property(self, "position:y", position.y - 100, 0.3)
+ tween.tween_property(self, "position:y", position.y + 500, 0.5)
+
+ died.emit()
+ GameManager.end_game()
+
+func collect_item(value: int, multiplier_bonus: float = 0.0) -> void:
+ GameManager.add_score(value)
+ if multiplier_bonus > 0:
+ GameManager.add_multiplier(multiplier_bonus)
+
+ # Flash effect
+ var original_color = sprite.color
+ sprite.color = Color(1, 1, 0, 1)
+ await get_tree().create_timer(0.1).timeout
+ sprite.color = original_color
+
+func _on_game_started() -> void:
+ is_dead = false
+ jumps_remaining = MAX_JUMPS
+ is_sliding = false
+ rotation = 0
+ sprite.modulate = Color.WHITE
+ sprite.scale = Vector2.ONE
+ position = Vector2(300, 800)
+
+func _on_game_over() -> void:
+ trail_particles.emitting = false
diff --git a/showcase_games/neon_runner/scripts/ui/game_ui.gd b/showcase_games/neon_runner/scripts/ui/game_ui.gd
new file mode 100644
index 00000000..99587005
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/ui/game_ui.gd
@@ -0,0 +1,98 @@
+extends Control
+## Game UI - HUD, pause menu, and game over screen
+
+@onready var score_label: Label = $ScoreLabel
+@onready var multiplier_label: Label = $MultiplierLabel
+@onready var pause_menu: Control = $PauseMenu
+@onready var game_over_menu: Control = $GameOverMenu
+
+@onready var resume_button: Button = $PauseMenu/VBox/ResumeButton
+@onready var restart_button: Button = $PauseMenu/VBox/RestartButton
+@onready var menu_button_pause: Button = $PauseMenu/VBox/MenuButton
+
+@onready var final_score_label: Label = $GameOverMenu/VBox/FinalScoreLabel
+@onready var high_score_label: Label = $GameOverMenu/VBox/HighScoreLabel
+@onready var new_high_score_label: Label = $GameOverMenu/VBox/NewHighScore
+@onready var retry_button: Button = $GameOverMenu/VBox/RetryButton
+@onready var menu_button_gameover: Button = $GameOverMenu/VBox/MenuButton
+
+var previous_high_score: int = 0
+
+func _ready() -> void:
+ # Connect GameManager signals
+ GameManager.score_changed.connect(_on_score_changed)
+ GameManager.multiplier_changed.connect(_on_multiplier_changed)
+ GameManager.game_over.connect(_on_game_over)
+ GameManager.game_started.connect(_on_game_started)
+
+ # Connect pause menu buttons
+ resume_button.pressed.connect(_on_resume_pressed)
+ restart_button.pressed.connect(_on_restart_pressed)
+ menu_button_pause.pressed.connect(_on_menu_pressed)
+
+ # Connect game over buttons
+ retry_button.pressed.connect(_on_restart_pressed)
+ menu_button_gameover.pressed.connect(_on_menu_pressed)
+
+ # Store initial high score
+ previous_high_score = GameManager.high_score
+
+func _process(_delta: float) -> void:
+ # Update pause menu visibility
+ pause_menu.visible = GameManager.is_paused
+
+func _on_score_changed(new_score: int) -> void:
+ score_label.text = str(new_score)
+
+ # Pulse animation on score change
+ var tween = create_tween()
+ tween.tween_property(score_label, "scale", Vector2(1.2, 1.2), 0.05)
+ tween.tween_property(score_label, "scale", Vector2(1.0, 1.0), 0.1)
+
+func _on_multiplier_changed(multiplier: float) -> void:
+ multiplier_label.text = "x%.1f" % multiplier
+
+ # Color based on multiplier
+ if multiplier >= 3.0:
+ multiplier_label.modulate = Color(1, 0.2, 0.6, 1) # Pink/hot
+ elif multiplier >= 2.0:
+ multiplier_label.modulate = Color(1, 0.5, 0, 1) # Orange
+ else:
+ multiplier_label.modulate = Color(1, 0.8, 0, 1) # Gold
+
+func _on_game_over() -> void:
+ await get_tree().create_timer(1.0).timeout
+
+ final_score_label.text = "SCORE: %d" % GameManager.score
+ high_score_label.text = "HIGH SCORE: %d" % GameManager.high_score
+
+ # Check for new high score
+ new_high_score_label.visible = GameManager.score > previous_high_score and GameManager.score == GameManager.high_score
+
+ # Animate game over menu
+ game_over_menu.modulate.a = 0.0
+ game_over_menu.visible = true
+
+ var tween = create_tween()
+ tween.tween_property(game_over_menu, "modulate:a", 1.0, 0.5)
+
+func _on_game_started() -> void:
+ game_over_menu.visible = false
+ pause_menu.visible = false
+ previous_high_score = GameManager.high_score
+
+ # Reset UI
+ score_label.text = "0"
+ multiplier_label.text = "x1.0"
+ multiplier_label.modulate = Color(1, 0.8, 0, 1)
+
+func _on_resume_pressed() -> void:
+ GameManager.resume_game()
+
+func _on_restart_pressed() -> void:
+ GameManager.resume_game() # Unpause if paused
+ get_parent().get_parent().restart_game()
+
+func _on_menu_pressed() -> void:
+ GameManager.resume_game() # Unpause if paused
+ get_parent().get_parent().return_to_menu()
diff --git a/showcase_games/neon_runner/scripts/ui/main_menu.gd b/showcase_games/neon_runner/scripts/ui/main_menu.gd
new file mode 100644
index 00000000..e0fc9958
--- /dev/null
+++ b/showcase_games/neon_runner/scripts/ui/main_menu.gd
@@ -0,0 +1,40 @@
+extends Control
+## Main Menu Controller
+
+@onready var play_button: Button = $VBox/PlayButton
+@onready var quit_button: Button = $VBox/QuitButton
+@ontml var high_score_label: Label = $VBox/HighScoreLabel
+@onready var title: Label = $VBox/Title
+
+var title_tween: Tween
+
+func _ready() -> void:
+ play_button.pressed.connect(_on_play_pressed)
+ quit_button.pressed.connect(_on_quit_pressed)
+
+ # Update high score display
+ high_score_label.text = "HIGH SCORE: %d" % GameManager.high_score
+ GameManager.high_score_changed.connect(_on_high_score_changed)
+
+ # Animate title with neon glow effect
+ _animate_title()
+
+func _animate_title() -> void:
+ title_tween = create_tween().set_loops()
+ title_tween.tween_property(title, "theme_override_colors/font_color",
+ Color(0, 1, 1, 1), 1.0)
+ title_tween.tween_property(title, "theme_override_colors/font_color",
+ Color(1, 0.2, 0.6, 1), 1.0)
+
+func _on_play_pressed() -> void:
+ # Transition to game
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 0.0, 0.5)
+ await tween.finished
+ get_tree().change_scene_to_file("res://scenes/game.tscn")
+
+func _on_quit_pressed() -> void:
+ get_tree().quit()
+
+func _on_high_score_changed(new_score: int) -> void:
+ high_score_label.text = "HIGH SCORE: %d" % new_score
diff --git a/showcase_games/void_explorer/icon.svg b/showcase_games/void_explorer/icon.svg
new file mode 100644
index 00000000..462244d8
--- /dev/null
+++ b/showcase_games/void_explorer/icon.svg
@@ -0,0 +1,28 @@
+
diff --git a/showcase_games/void_explorer/project.godot b/showcase_games/void_explorer/project.godot
new file mode 100644
index 00000000..95d76e35
--- /dev/null
+++ b/showcase_games/void_explorer/project.godot
@@ -0,0 +1,96 @@
+; 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="Void Explorer"
+config/description="A first-person space station exploration game. Investigate an abandoned station, solve environmental puzzles, and uncover what happened to the crew."
+run/main_scene="res://scenes/main_menu.tscn"
+config/features=PackedStringArray("4.7", "Forward Plus")
+config/icon="res://icon.svg"
+
+[autoload]
+
+GameState="*res://scripts/autoload/game_state.gd"
+SaveManager="*res://scripts/autoload/save_manager.gd"
+
+[display]
+
+window/size/viewport_width=1920
+window/size/viewport_height=1080
+window/stretch/mode="canvas_items"
+
+[input]
+
+move_forward={
+"deadzone": 0.5,
+"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)
+]
+}
+move_back={
+"deadzone": 0.5,
+"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)
+]
+}
+move_left={
+"deadzone": 0.5,
+"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)
+]
+}
+move_right={
+"deadzone": 0.5,
+"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)
+]
+}
+interact={
+"deadzone": 0.5,
+"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)
+, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+flashlight={
+"deadzone": 0.5,
+"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":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
+]
+}
+sprint={
+"deadzone": 0.5,
+"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":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+crouch={
+"deadzone": 0.5,
+"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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+journal={
+"deadzone": 0.5,
+"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":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
+]
+}
+pause={
+"deadzone": 0.5,
+"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":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+
+[layer_names]
+
+3d_physics/layer_1="player"
+3d_physics/layer_2="world"
+3d_physics/layer_3="interactables"
+3d_physics/layer_4="triggers"
+
+[rendering]
+
+renderer/rendering_method="forward_plus"
+lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3
+environment/defaults/default_clear_color=Color(0.01, 0.01, 0.02, 1)
+anti_aliasing/quality/msaa_3d=2
diff --git a/showcase_games/void_explorer/scenes/main_menu.tscn b/showcase_games/void_explorer/scenes/main_menu.tscn
new file mode 100644
index 00000000..422d256b
--- /dev/null
+++ b/showcase_games/void_explorer/scenes/main_menu.tscn
@@ -0,0 +1,101 @@
+[gd_scene load_steps=2 format=3 uid="uid://void_main_menu"]
+
+[ext_resource type="Script" path="res://scripts/ui/main_menu.gd" id="1_menu"]
+
+[node name="MainMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("1_menu")
+
+[node name="Background" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color(0.01, 0.01, 0.02, 1)
+
+[node name="Stars" type="Control" parent="Background"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -180.0
+offset_right = 200.0
+offset_bottom = 180.0
+theme_override_constants/separation = 15
+
+[node name="Title" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.4, 0.6, 1, 1)
+theme_override_colors/font_outline_color = Color(0.1, 0.2, 0.4, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 64
+text = "VOID EXPLORER"
+horizontal_alignment = 1
+
+[node name="Subtitle" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.6, 0.7, 0.9, 0.7)
+theme_override_font_sizes/font_size = 16
+text = "What happened to the crew of Station Erebus?"
+horizontal_alignment = 1
+
+[node name="Spacer" type="Control" parent="VBox"]
+custom_minimum_size = Vector2(0, 40)
+layout_mode = 2
+
+[node name="NewGameButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(300, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 22
+text = "NEW GAME"
+
+[node name="ContinueButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(300, 50)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 22
+text = "CONTINUE"
+
+[node name="SettingsButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(250, 40)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 18
+text = "SETTINGS"
+
+[node name="QuitButton" type="Button" parent="VBox"]
+custom_minimum_size = Vector2(200, 40)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 18
+text = "QUIT"
+
+[node name="Version" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -150.0
+offset_top = -30.0
+offset_right = -10.0
+grow_horizontal = 0
+grow_vertical = 0
+theme_override_colors/font_color = Color(1, 1, 1, 0.3)
+theme_override_font_sizes/font_size = 12
+text = "Made with AeThex Engine"
+horizontal_alignment = 2
diff --git a/showcase_games/void_explorer/scenes/player.tscn b/showcase_games/void_explorer/scenes/player.tscn
new file mode 100644
index 00000000..9e42fdf6
--- /dev/null
+++ b/showcase_games/void_explorer/scenes/player.tscn
@@ -0,0 +1,46 @@
+[gd_scene load_steps=3 format=3 uid="uid://void_player"]
+
+[ext_resource type="Script" path="res://scripts/player/player_controller.gd" id="1_player"]
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_standing"]
+height = 1.8
+radius = 0.4
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_crouch"]
+height = 1.0
+radius = 0.4
+
+[node name="Player" type="CharacterBody3D"]
+collision_layer = 1
+collision_mask = 2
+script = ExtResource("1_player")
+
+[node name="StandingCollision" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
+shape = SubResource("CapsuleShape3D_standing")
+
+[node name="CrouchCollision" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
+disabled = true
+shape = SubResource("CapsuleShape3D_crouch")
+
+[node name="Head" type="Node3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0)
+
+[node name="Camera3D" type="Camera3D" parent="Head"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
+fov = 75.0
+
+[node name="Flashlight" type="SpotLight3D" parent="Head/Camera3D"]
+visible = false
+light_color = Color(0.95, 0.95, 1, 1)
+light_energy = 2.0
+light_indirect_energy = 0.0
+shadow_enabled = true
+spot_range = 20.0
+spot_attenuation = 0.5
+spot_angle = 25.0
+
+[node name="InteractionRay" type="RayCast3D" parent="Head/Camera3D"]
+target_position = Vector3(0, 0, -3)
+collision_mask = 4
diff --git a/showcase_games/void_explorer/scenes/station.tscn b/showcase_games/void_explorer/scenes/station.tscn
new file mode 100644
index 00000000..2e3505fc
--- /dev/null
+++ b/showcase_games/void_explorer/scenes/station.tscn
@@ -0,0 +1,188 @@
+[gd_scene load_steps=6 format=3 uid="uid://void_station"]
+
+[ext_resource type="PackedScene" uid="uid://void_player" path="res://scenes/player.tscn" id="1_player"]
+[ext_resource type="Script" path="res://scripts/station/station_controller.gd" id="2_station"]
+[ext_resource type="Script" path="res://scripts/ui/game_ui.gd" id="3_ui"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_space"]
+sky_top_color = Color(0, 0, 0.02, 1)
+sky_horizon_color = Color(0.01, 0.01, 0.05, 1)
+ground_bottom_color = Color(0, 0, 0, 1)
+ground_horizon_color = Color(0.01, 0.01, 0.03, 1)
+
+[sub_resource type="Sky" id="Sky_space"]
+sky_material = SubResource("ProceduralSkyMaterial_space")
+
+[sub_resource type="Environment" id="Environment_station"]
+background_mode = 2
+sky = SubResource("Sky_space")
+ambient_light_source = 1
+ambient_light_color = Color(0.05, 0.05, 0.1, 1)
+ambient_light_energy = 0.3
+tonemap_mode = 2
+glow_enabled = true
+glow_intensity = 0.5
+glow_bloom = 0.3
+fog_enabled = true
+fog_light_color = Color(0.02, 0.02, 0.05, 1)
+fog_density = 0.01
+volumetric_fog_enabled = true
+volumetric_fog_density = 0.02
+volumetric_fog_emission = Color(0.05, 0.05, 0.1, 1)
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
+size = Vector3(50, 1, 50)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
+albedo_color = Color(0.1, 0.1, 0.15, 1)
+metallic = 0.3
+roughness = 0.7
+
+[sub_resource type="BoxMesh" id="BoxMesh_floor"]
+material = SubResource("StandardMaterial3D_floor")
+size = Vector3(50, 1, 50)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
+albedo_color = Color(0.15, 0.15, 0.2, 1)
+metallic = 0.2
+roughness = 0.8
+
+[node name="Station" type="Node3D"]
+script = ExtResource("2_station")
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_station")
+
+[node name="Player" parent="." instance=ExtResource("1_player")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+
+[node name="StationGeometry" type="Node3D" parent="."]
+
+[node name="Floor" type="StaticBody3D" parent="StationGeometry"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
+collision_layer = 2
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="StationGeometry/Floor"]
+shape = SubResource("BoxShape3D_floor")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="StationGeometry/Floor"]
+mesh = SubResource("BoxMesh_floor")
+
+[node name="Walls" type="Node3D" parent="StationGeometry"]
+
+[node name="NorthWall" type="StaticBody3D" parent="StationGeometry/Walls"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, -25)
+collision_layer = 2
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="StationGeometry/Walls/NorthWall"]
+shape = SubResource("BoxShape3D_wall")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="StationGeometry/Walls/NorthWall"]
+mesh = SubResource("BoxMesh_wall")
+
+[node name="SouthWall" type="StaticBody3D" parent="StationGeometry/Walls"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 25)
+collision_layer = 2
+
+[node name="EastWall" type="StaticBody3D" parent="StationGeometry/Walls"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 25, 2, 0)
+collision_layer = 2
+
+[node name="WestWall" type="StaticBody3D" parent="StationGeometry/Walls"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 2, 0)
+collision_layer = 2
+
+[node name="Lighting" type="Node3D" parent="."]
+
+[node name="EmergencyLight1" type="OmniLight3D" parent="Lighting"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 3, 5)
+light_color = Color(1, 0.2, 0.1, 1)
+light_energy = 0.5
+omni_range = 8.0
+
+[node name="EmergencyLight2" type="OmniLight3D" parent="Lighting"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 3, -5)
+light_color = Color(1, 0.2, 0.1, 1)
+light_energy = 0.5
+omni_range = 8.0
+
+[node name="ConsoleLight" type="OmniLight3D" parent="Lighting"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -10)
+light_color = Color(0.3, 0.6, 1, 1)
+light_energy = 0.8
+omni_range = 5.0
+
+[node name="Interactables" type="Node3D" parent="."]
+
+[node name="CanvasLayer" type="CanvasLayer" parent="."]
+
+[node name="GameUI" type="Control" parent="CanvasLayer"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("3_ui")
+
+[node name="Crosshair" type="ColorRect" parent="CanvasLayer/GameUI"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -2.0
+offset_top = -2.0
+offset_right = 2.0
+offset_bottom = 2.0
+color = Color(1, 1, 1, 0.5)
+
+[node name="InteractPrompt" type="Label" parent="CanvasLayer/GameUI"]
+visible = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -100.0
+offset_top = 30.0
+offset_right = 100.0
+offset_bottom = 60.0
+theme_override_colors/font_color = Color(1, 1, 1, 0.8)
+theme_override_font_sizes/font_size = 16
+text = "[E] Interact"
+horizontal_alignment = 1
+
+[node name="ObjectivePanel" type="PanelContainer" parent="CanvasLayer/GameUI"]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -300.0
+offset_top = 20.0
+offset_right = -20.0
+offset_bottom = 100.0
+
+[node name="VBox" type="VBoxContainer" parent="CanvasLayer/GameUI/ObjectivePanel"]
+layout_mode = 2
+theme_override_constants/separation = 5
+
+[node name="ObjectiveTitle" type="Label" parent="CanvasLayer/GameUI/ObjectivePanel/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.4, 0.6, 1, 1)
+theme_override_font_sizes/font_size = 14
+text = "OBJECTIVE"
+
+[node name="ObjectiveText" type="Label" parent="CanvasLayer/GameUI/ObjectivePanel/VBox"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.8, 0.9, 1, 1)
+theme_override_font_sizes/font_size = 16
+text = "Explore the station"
+autowrap_mode = 2
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_wall"]
+size = Vector3(50, 5, 1)
+
+[sub_resource type="BoxMesh" id="BoxMesh_wall"]
+material = SubResource("StandardMaterial3D_wall")
+size = Vector3(50, 5, 1)
diff --git a/showcase_games/void_explorer/scripts/autoload/game_state.gd b/showcase_games/void_explorer/scripts/autoload/game_state.gd
new file mode 100644
index 00000000..fd1809e6
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/autoload/game_state.gd
@@ -0,0 +1,101 @@
+extends Node
+## Game State - Tracks exploration progress, inventory, and story state
+
+signal item_collected(item_id: String)
+signal log_discovered(log_id: String)
+signal objective_updated(objective: String)
+signal power_restored(section: String)
+
+# Player inventory
+var inventory: Array[String] = []
+
+# Discovered logs/audio recordings
+var discovered_logs: Array[String] = []
+
+# Story progress flags
+var story_flags: Dictionary = {
+ "intro_complete": false,
+ "found_first_log": false,
+ "power_section_a": false,
+ "power_section_b": false,
+ "power_section_c": false,
+ "found_keycard_red": false,
+ "found_keycard_blue": false,
+ "found_keycard_gold": false,
+ "opened_lab": false,
+ "opened_bridge": false,
+ "found_truth": false,
+ "game_complete": false,
+}
+
+# Current objectives
+var current_objective: String = "Explore the station"
+var completed_objectives: Array[String] = []
+
+# Player stats
+var total_logs: int = 12
+var total_items: int = 15
+
+func _ready() -> void:
+ pass
+
+func has_item(item_id: String) -> bool:
+ return item_id in inventory
+
+func add_item(item_id: String) -> void:
+ if item_id not in inventory:
+ inventory.append(item_id)
+ item_collected.emit(item_id)
+
+func remove_item(item_id: String) -> void:
+ inventory.erase(item_id)
+
+func discover_log(log_id: String) -> void:
+ if log_id not in discovered_logs:
+ discovered_logs.append(log_id)
+ log_discovered.emit(log_id)
+
+ if not story_flags["found_first_log"]:
+ set_flag("found_first_log", true)
+ set_objective("Find more crew logs to piece together what happened")
+
+func set_flag(flag: String, value: bool) -> void:
+ if flag in story_flags:
+ story_flags[flag] = value
+
+ # Check for power restoration
+ if flag.begins_with("power_section_"):
+ power_restored.emit(flag.replace("power_section_", ""))
+ _check_all_power()
+
+func get_flag(flag: String) -> bool:
+ return story_flags.get(flag, false)
+
+func set_objective(objective: String) -> void:
+ if current_objective != "":
+ completed_objectives.append(current_objective)
+ current_objective = objective
+ objective_updated.emit(objective)
+
+func _check_all_power() -> void:
+ if story_flags["power_section_a"] and story_flags["power_section_b"] and story_flags["power_section_c"]:
+ set_objective("All power restored - Access the bridge")
+
+func get_completion_percentage() -> float:
+ var logs = discovered_logs.size() / float(total_logs)
+ var items = inventory.size() / float(total_items)
+ var flags = 0
+ for flag in story_flags.values():
+ if flag:
+ flags += 1
+ var flag_percent = flags / float(story_flags.size())
+
+ return (logs + items + flag_percent) / 3.0 * 100.0
+
+func reset() -> void:
+ inventory.clear()
+ discovered_logs.clear()
+ for flag in story_flags.keys():
+ story_flags[flag] = false
+ current_objective = "Explore the station"
+ completed_objectives.clear()
diff --git a/showcase_games/void_explorer/scripts/autoload/save_manager.gd b/showcase_games/void_explorer/scripts/autoload/save_manager.gd
new file mode 100644
index 00000000..25a51f59
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/autoload/save_manager.gd
@@ -0,0 +1,93 @@
+extends Node
+## Save Manager - Handles game saves and loads
+
+const SAVE_PATH = "user://void_explorer_save.json"
+const SETTINGS_PATH = "user://void_explorer_settings.json"
+
+signal save_completed
+signal load_completed
+
+var settings: Dictionary = {
+ "mouse_sensitivity": 0.3,
+ "invert_y": false,
+ "master_volume": 1.0,
+ "music_volume": 0.8,
+ "sfx_volume": 1.0,
+ "graphics_quality": 2, # 0=Low, 1=Medium, 2=High
+}
+
+func _ready() -> void:
+ load_settings()
+
+func save_game(player_position: Vector3, player_rotation: Vector3) -> void:
+ var save_data = {
+ "version": 1,
+ "timestamp": Time.get_datetime_string_from_system(),
+ "player": {
+ "position": {"x": player_position.x, "y": player_position.y, "z": player_position.z},
+ "rotation": {"x": player_rotation.x, "y": player_rotation.y, "z": player_rotation.z},
+ },
+ "inventory": GameState.inventory,
+ "discovered_logs": GameState.discovered_logs,
+ "story_flags": GameState.story_flags,
+ "current_objective": GameState.current_objective,
+ "completed_objectives": GameState.completed_objectives,
+ }
+
+ var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
+ if file:
+ file.store_string(JSON.stringify(save_data, "\t"))
+ save_completed.emit()
+
+func load_game() -> Dictionary:
+ if not FileAccess.file_exists(SAVE_PATH):
+ return {}
+
+ var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if not file:
+ return {}
+
+ var json = JSON.new()
+ var error = json.parse(file.get_as_text())
+ if error != OK:
+ return {}
+
+ var save_data = json.get_data()
+
+ # Restore game state
+ GameState.inventory = save_data.get("inventory", [])
+ GameState.discovered_logs = save_data.get("discovered_logs", [])
+ GameState.story_flags = save_data.get("story_flags", GameState.story_flags)
+ GameState.current_objective = save_data.get("current_objective", "Explore the station")
+ GameState.completed_objectives = save_data.get("completed_objectives", [])
+
+ load_completed.emit()
+ return save_data
+
+func has_save() -> bool:
+ return FileAccess.file_exists(SAVE_PATH)
+
+func delete_save() -> void:
+ if FileAccess.file_exists(SAVE_PATH):
+ DirAccess.remove_absolute(SAVE_PATH)
+
+func save_settings() -> void:
+ var file = FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
+ if file:
+ file.store_string(JSON.stringify(settings, "\t"))
+
+func load_settings() -> void:
+ if not FileAccess.file_exists(SETTINGS_PATH):
+ return
+
+ var file = FileAccess.open(SETTINGS_PATH, FileAccess.READ)
+ if not file:
+ return
+
+ var json = JSON.new()
+ var error = json.parse(file.get_as_text())
+ if error == OK:
+ var loaded = json.get_data()
+ for key in loaded.keys():
+ if key in settings:
+ settings[key] = loaded[key]
diff --git a/showcase_games/void_explorer/scripts/player/player_controller.gd b/showcase_games/void_explorer/scripts/player/player_controller.gd
new file mode 100644
index 00000000..f98d9814
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/player/player_controller.gd
@@ -0,0 +1,149 @@
+extends CharacterBody3D
+class_name VoidPlayer
+## First-person player controller for space station exploration
+
+signal interacted_with(object: Node3D)
+
+const WALK_SPEED: float = 4.0
+const SPRINT_SPEED: float = 7.0
+const CROUCH_SPEED: float = 2.0
+const JUMP_VELOCITY: float = 4.5
+const GRAVITY: float = 9.8
+
+@export var mouse_sensitivity: float = 0.3
+
+@onready var head: Node3D = $Head
+@onready var camera: Camera3D = $Head/Camera3D
+@onready var flashlight: SpotLight3D = $Head/Camera3D/Flashlight
+@onready var interaction_ray: RayCast3D = $Head/Camera3D/InteractionRay
+@onready var standing_collision: CollisionShape3D = $StandingCollision
+@onready var crouch_collision: CollisionShape3D = $CrouchCollision
+
+var current_speed: float = WALK_SPEED
+var is_crouching: bool = false
+var flashlight_on: bool = false
+var can_interact: bool = false
+var interaction_target: Node3D = null
+
+# Head bob variables
+var head_bob_timer: float = 0.0
+var head_bob_amount: float = 0.05
+var head_bob_frequency: float = 2.0
+
+func _ready() -> void:
+ Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
+ flashlight.visible = false
+
+ # Apply settings
+ mouse_sensitivity = SaveManager.settings.get("mouse_sensitivity", 0.3)
+
+func _input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
+ var invert = -1 if SaveManager.settings.get("invert_y", false) else 1
+ rotate_y(deg_to_rad(-event.relative.x * mouse_sensitivity))
+ head.rotate_x(deg_to_rad(-event.relative.y * mouse_sensitivity * invert))
+ head.rotation.x = clamp(head.rotation.x, deg_to_rad(-89), deg_to_rad(89))
+
+ if event.is_action_pressed("flashlight"):
+ _toggle_flashlight()
+
+ if event.is_action_pressed("interact") and can_interact and interaction_target:
+ _interact()
+
+func _physics_process(delta: float) -> void:
+ # Gravity
+ if not is_on_floor():
+ velocity.y -= GRAVITY * delta
+
+ # Handle sprint
+ if Input.is_action_pressed("sprint") and not is_crouching:
+ current_speed = SPRINT_SPEED
+ elif is_crouching:
+ current_speed = CROUCH_SPEED
+ else:
+ current_speed = WALK_SPEED
+
+ # Handle crouch
+ if Input.is_action_just_pressed("crouch"):
+ _start_crouch()
+ elif Input.is_action_just_released("crouch"):
+ _end_crouch()
+
+ # Get input direction
+ var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
+ var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
+
+ # Apply movement
+ if direction:
+ velocity.x = direction.x * current_speed
+ velocity.z = direction.z * current_speed
+
+ # Head bob when moving
+ if is_on_floor():
+ _apply_head_bob(delta)
+ else:
+ velocity.x = move_toward(velocity.x, 0, current_speed)
+ velocity.z = move_toward(velocity.z, 0, current_speed)
+
+ move_and_slide()
+
+ # Check for interactables
+ _check_interaction()
+
+func _apply_head_bob(delta: float) -> void:
+ head_bob_timer += delta * head_bob_frequency * (current_speed / WALK_SPEED)
+ var bob_offset = sin(head_bob_timer) * head_bob_amount
+ camera.position.y = 0.6 + bob_offset
+
+func _check_interaction() -> void:
+ if interaction_ray.is_colliding():
+ var collider = interaction_ray.get_collider()
+ if collider.is_in_group("interactable"):
+ can_interact = true
+ interaction_target = collider
+ return
+
+ can_interact = false
+ interaction_target = null
+
+func _interact() -> void:
+ if interaction_target and interaction_target.has_method("interact"):
+ interaction_target.interact(self)
+ interacted_with.emit(interaction_target)
+
+func _toggle_flashlight() -> void:
+ flashlight_on = not flashlight_on
+ flashlight.visible = flashlight_on
+
+ # Flashlight flicker effect on toggle
+ if flashlight_on:
+ var tween = create_tween()
+ tween.tween_property(flashlight, "light_energy", 0.0, 0.05)
+ tween.tween_property(flashlight, "light_energy", 2.0, 0.05)
+ tween.tween_property(flashlight, "light_energy", 0.5, 0.05)
+ tween.tween_property(flashlight, "light_energy", 2.0, 0.1)
+
+func _start_crouch() -> void:
+ is_crouching = true
+ standing_collision.disabled = true
+ crouch_collision.disabled = false
+
+ var tween = create_tween()
+ tween.tween_property(head, "position:y", 0.8, 0.2)
+
+func _end_crouch() -> void:
+ # Check if there's room to stand
+ # Simplified - just uncrouch
+ is_crouching = false
+ standing_collision.disabled = false
+ crouch_collision.disabled = true
+
+ var tween = create_tween()
+ tween.tween_property(head, "position:y", 1.6, 0.2)
+
+func collect_item(item_id: String) -> void:
+ GameState.add_item(item_id)
+
+func read_log(log_id: String, content: String) -> void:
+ GameState.discover_log(log_id)
+ # UI should display the log content
diff --git a/showcase_games/void_explorer/scripts/station/station_controller.gd b/showcase_games/void_explorer/scripts/station/station_controller.gd
new file mode 100644
index 00000000..d26a0d1d
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/station/station_controller.gd
@@ -0,0 +1,164 @@
+extends Node3D
+## Station Controller - Manages the space station environment
+
+@onready var player: VoidPlayer = $Player
+@onready var lighting: Node3D = $Lighting
+@onready var interactables: Node3D = $Interactables
+
+var emergency_lights: Array[OmniLight3D] = []
+var emergency_light_timer: float = 0.0
+
+func _ready() -> void:
+ # Collect emergency lights
+ for child in lighting.get_children():
+ if child is OmniLight3D and "Emergency" in child.name:
+ emergency_lights.append(child)
+
+ # Connect game state signals
+ GameState.power_restored.connect(_on_power_restored)
+
+ # Set up procedural elements
+ _spawn_interactables()
+
+ # Initialize atmosphere
+ _setup_atmosphere()
+
+func _process(delta: float) -> void:
+ # Emergency light flicker
+ emergency_light_timer += delta
+ if emergency_light_timer > 0.5:
+ emergency_light_timer = 0.0
+ _flicker_emergency_lights()
+
+func _flicker_emergency_lights() -> void:
+ for light in emergency_lights:
+ if randf() > 0.7:
+ var original = light.light_energy
+ light.light_energy = 0.0
+ await get_tree().create_timer(randf_range(0.05, 0.15)).timeout
+ light.light_energy = original
+
+func _spawn_interactables() -> void:
+ # Spawn crew logs
+ _spawn_log(Vector3(5, 1, -8), "log_001", "Commander's Log - Day 1",
+ "Station Erebus is now fully operational. The crew is in good spirits. We've begun our research into the anomaly detected in sector 7...")
+
+ _spawn_log(Vector3(-8, 1, 3), "log_002", "Engineer's Notes",
+ "The power fluctuations are getting worse. I've rerouted auxiliary power but something is draining the main reactor...")
+
+ _spawn_log(Vector3(12, 1, 12), "log_003", "Dr. Chen's Research Notes",
+ "The samples we collected from the anomaly... they're not behaving like normal matter. They seem to react to human presence...")
+
+ # Spawn keycards
+ _spawn_keycard(Vector3(-5, 1, -12), "keycard_red", Color.RED)
+ _spawn_keycard(Vector3(15, 1, 8), "keycard_blue", Color.BLUE)
+
+ # Spawn power switches
+ _spawn_power_switch(Vector3(20, 1.5, 0), "power_section_a", "Section A")
+ _spawn_power_switch(Vector3(-20, 1.5, 10), "power_section_b", "Section B")
+ _spawn_power_switch(Vector3(0, 1.5, 20), "power_section_c", "Section C")
+
+func _spawn_log(pos: Vector3, log_id: String, title: String, content: String) -> void:
+ var log_obj = _create_interactable_base(pos, Color(0.2, 0.4, 0.8))
+ log_obj.name = "Log_" + log_id
+ log_obj.set_meta("log_id", log_id)
+ log_obj.set_meta("title", title)
+ log_obj.set_meta("content", content)
+ log_obj.set_meta("type", "log")
+
+ # Add glow
+ var light = OmniLight3D.new()
+ light.light_color = Color(0.2, 0.4, 1.0)
+ light.light_energy = 0.5
+ light.omni_range = 2.0
+ log_obj.add_child(light)
+
+ interactables.add_child(log_obj)
+
+func _spawn_keycard(pos: Vector3, item_id: String, color: Color) -> void:
+ var card = _create_interactable_base(pos, color)
+ card.name = "Keycard_" + item_id
+ card.set_meta("item_id", item_id)
+ card.set_meta("type", "keycard")
+ card.scale = Vector3(0.3, 0.05, 0.2)
+
+ interactables.add_child(card)
+
+func _spawn_power_switch(pos: Vector3, flag_id: String, label: String) -> void:
+ var switch_obj = _create_interactable_base(pos, Color(0.8, 0.2, 0.2))
+ switch_obj.name = "Switch_" + flag_id
+ switch_obj.set_meta("flag_id", flag_id)
+ switch_obj.set_meta("label", label)
+ switch_obj.set_meta("type", "switch")
+ switch_obj.set_meta("activated", false)
+ switch_obj.scale = Vector3(0.3, 0.5, 0.1)
+
+ interactables.add_child(switch_obj)
+
+func _create_interactable_base(pos: Vector3, color: Color) -> StaticBody3D:
+ var body = StaticBody3D.new()
+ body.position = pos
+ body.collision_layer = 4
+ body.add_to_group("interactable")
+
+ var shape = CollisionShape3D.new()
+ var box = BoxShape3D.new()
+ box.size = Vector3(0.5, 0.5, 0.5)
+ shape.shape = box
+ body.add_child(shape)
+
+ var mesh_instance = MeshInstance3D.new()
+ var mesh = BoxMesh.new()
+ mesh.size = Vector3(0.5, 0.5, 0.5)
+ var material = StandardMaterial3D.new()
+ material.albedo_color = color
+ material.emission_enabled = true
+ material.emission = color * 0.3
+ material.emission_energy_multiplier = 0.5
+ mesh.material = material
+ mesh_instance.mesh = mesh
+ body.add_child(mesh_instance)
+
+ # Add interact method via script
+ var script = GDScript.new()
+ script.source_code = """
+extends StaticBody3D
+
+func interact(player):
+ var obj_type = get_meta("type", "")
+ match obj_type:
+ "log":
+ player.read_log(get_meta("log_id"), get_meta("content"))
+ # Visual feedback
+ var tween = create_tween()
+ tween.tween_property(self, "scale", scale * 1.2, 0.1)
+ tween.tween_property(self, "scale", scale, 0.1)
+ "keycard":
+ player.collect_item(get_meta("item_id"))
+ queue_free()
+ "switch":
+ if not get_meta("activated"):
+ set_meta("activated", true)
+ GameState.set_flag(get_meta("flag_id"), true)
+ # Turn green
+ $MeshInstance3D.mesh.material.albedo_color = Color.GREEN
+ $MeshInstance3D.mesh.material.emission = Color.GREEN * 0.5
+"""
+ body.set_script(script)
+
+ return body
+
+func _on_power_restored(section: String) -> void:
+ # Add lights for restored section
+ print("Power restored to: ", section)
+
+func _setup_atmosphere() -> void:
+ # Add ambient sounds, particles, etc.
+ pass
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed("pause"):
+ if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
+ Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
+ else:
+ Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
diff --git a/showcase_games/void_explorer/scripts/ui/game_ui.gd b/showcase_games/void_explorer/scripts/ui/game_ui.gd
new file mode 100644
index 00000000..8111c665
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/ui/game_ui.gd
@@ -0,0 +1,68 @@
+extends Control
+## Game UI for Void Explorer
+
+@onready var crosshair: ColorRect = $Crosshair
+@onready var interact_prompt: Label = $InteractPrompt
+@onready var objective_text: Label = $ObjectivePanel/VBox/ObjectiveText
+
+var player: VoidPlayer
+
+func _ready() -> void:
+ GameState.objective_updated.connect(_on_objective_updated)
+ GameState.item_collected.connect(_on_item_collected)
+ GameState.log_discovered.connect(_on_log_discovered)
+
+ # Get player reference
+ await get_tree().process_frame
+ player = get_tree().get_first_node_in_group("player")
+ if not player:
+ var players = get_tree().get_nodes_in_group("player")
+ if players.size() > 0:
+ player = players[0]
+
+ # Update objective
+ objective_text.text = GameState.current_objective
+
+func _process(_delta: float) -> void:
+ if player:
+ # Update interact prompt
+ interact_prompt.visible = player.can_interact
+
+ # Update crosshair color based on interaction
+ if player.can_interact:
+ crosshair.color = Color(0.4, 0.8, 1, 0.8)
+ else:
+ crosshair.color = Color(1, 1, 1, 0.5)
+
+func _on_objective_updated(objective: String) -> void:
+ objective_text.text = objective
+
+ # Flash effect
+ var original_color = objective_text.modulate
+ objective_text.modulate = Color.YELLOW
+ var tween = create_tween()
+ tween.tween_property(objective_text, "modulate", original_color, 0.5)
+
+func _on_item_collected(item_id: String) -> void:
+ # Show pickup notification
+ _show_notification("Collected: " + item_id.replace("_", " ").capitalize())
+
+func _on_log_discovered(log_id: String) -> void:
+ _show_notification("New Log Discovered")
+
+func _show_notification(text: String) -> void:
+ var notification = Label.new()
+ notification.text = text
+ notification.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ notification.add_theme_font_size_override("font_size", 24)
+ notification.add_theme_color_override("font_color", Color(0.4, 0.8, 1, 1))
+ notification.position = Vector2(get_viewport_rect().size.x / 2 - 150, get_viewport_rect().size.y - 150)
+ notification.size = Vector2(300, 40)
+ notification.modulate.a = 0.0
+ add_child(notification)
+
+ var tween = create_tween()
+ tween.tween_property(notification, "modulate:a", 1.0, 0.3)
+ tween.tween_interval(2.0)
+ tween.tween_property(notification, "modulate:a", 0.0, 0.5)
+ tween.tween_callback(notification.queue_free)
diff --git a/showcase_games/void_explorer/scripts/ui/main_menu.gd b/showcase_games/void_explorer/scripts/ui/main_menu.gd
new file mode 100644
index 00000000..d88e51ca
--- /dev/null
+++ b/showcase_games/void_explorer/scripts/ui/main_menu.gd
@@ -0,0 +1,63 @@
+extends Control
+## Main Menu for Void Explorer
+
+@onready var new_game_button: Button = $VBox/NewGameButton
+@onready var continue_button: Button = $VBox/ContinueButton
+@onready var settings_button: Button = $VBox/SettingsButton
+@onready var quit_button: Button = $VBox/QuitButton
+@onready var stars_container: Control = $Background/Stars
+
+var star_positions: Array[Vector2] = []
+
+func _ready() -> void:
+ new_game_button.pressed.connect(_on_new_game)
+ continue_button.pressed.connect(_on_continue)
+ settings_button.pressed.connect(_on_settings)
+ quit_button.pressed.connect(_on_quit)
+
+ # Enable/disable continue based on save existence
+ continue_button.disabled = not SaveManager.has_save()
+
+ # Generate stars
+ _generate_stars()
+
+ # Fade in
+ modulate.a = 0.0
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 1.0, 1.0)
+
+func _generate_stars() -> void:
+ var viewport_size = get_viewport_rect().size
+ for i in 100:
+ var star = ColorRect.new()
+ star.size = Vector2(2, 2) if randf() > 0.7 else Vector2(1, 1)
+ star.position = Vector2(randf() * viewport_size.x, randf() * viewport_size.y)
+ star.color = Color(1, 1, 1, randf_range(0.3, 1.0))
+ stars_container.add_child(star)
+ star_positions.append(star.position)
+
+ # Twinkle animation
+ var tween = create_tween().set_loops()
+ tween.tween_property(star, "modulate:a", randf_range(0.3, 0.7), randf_range(1.0, 3.0))
+ tween.tween_property(star, "modulate:a", 1.0, randf_range(1.0, 3.0))
+
+func _on_new_game() -> void:
+ GameState.reset()
+ _transition_to_game()
+
+func _on_continue() -> void:
+ SaveManager.load_game()
+ _transition_to_game()
+
+func _transition_to_game() -> void:
+ var tween = create_tween()
+ tween.tween_property(self, "modulate:a", 0.0, 0.5)
+ await tween.finished
+ get_tree().change_scene_to_file("res://scenes/station.tscn")
+
+func _on_settings() -> void:
+ # TODO: Open settings panel
+ pass
+
+func _on_quit() -> void:
+ get_tree().quit()