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 @@ + + + + + + + + + + + + + + + + + + + + + + CIRCUIT + 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 @@ + + + + + + + + + + + + NEON + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + VOID + 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()