Add 3 showcase game projects - Neon Runner: 2D synthwave endless runner - Void Explorer: 3D first-person space station exploration - Circuit Logic: Minimalist puzzle game about connecting circuits
This commit is contained in:
parent
39c99577f5
commit
782e1d35e0
33 changed files with 3767 additions and 0 deletions
23
showcase_games/circuit_logic/icon.svg
Normal file
23
showcase_games/circuit_logic/icon.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" fill="#1e2228"/>
|
||||
<!-- Circuit grid -->
|
||||
<line x1="20" y1="64" x2="50" y2="64" stroke="#3a3f4a" stroke-width="4"/>
|
||||
<line x1="78" y1="64" x2="108" y2="64" stroke="#4dcc80" stroke-width="4"/>
|
||||
<line x1="64" y1="30" x2="64" y2="50" stroke="#4dcc80" stroke-width="4"/>
|
||||
<line x1="64" y1="78" x2="64" y2="98" stroke="#3a3f4a" stroke-width="4"/>
|
||||
<!-- Center cross -->
|
||||
<rect x="50" y="50" width="28" height="28" rx="4" fill="#2a2f38"/>
|
||||
<line x1="50" y1="64" x2="78" y2="64" stroke="#4dcc80" stroke-width="4"/>
|
||||
<line x1="64" y1="50" x2="64" y2="78" stroke="#4dcc80" stroke-width="4"/>
|
||||
<circle cx="64" cy="64" r="6" fill="#4dcc80"/>
|
||||
<!-- Source -->
|
||||
<rect x="10" y="54" width="20" height="20" rx="3" fill="#2a2f38"/>
|
||||
<circle cx="20" cy="64" r="6" fill="#ffd54f"/>
|
||||
<!-- Output -->
|
||||
<rect x="98" y="54" width="20" height="20" rx="3" fill="#2a2f38"/>
|
||||
<circle cx="108" cy="64" r="6" fill="#4dcc80"/>
|
||||
<!-- Corner pieces -->
|
||||
<rect x="54" y="10" width="20" height="20" rx="3" fill="#2a2f38"/>
|
||||
<rect x="54" y="98" width="20" height="20" rx="3" fill="#2a2f38"/>
|
||||
<text x="64" y="122" font-family="Arial" font-size="8" fill="#4dcc80" text-anchor="middle">CIRCUIT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
62
showcase_games/circuit_logic/project.godot
Normal file
62
showcase_games/circuit_logic/project.godot
Normal file
|
|
@ -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)
|
||||
149
showcase_games/circuit_logic/scenes/game.tscn
Normal file
149
showcase_games/circuit_logic/scenes/game.tscn
Normal file
|
|
@ -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"
|
||||
79
showcase_games/circuit_logic/scenes/level_select.tscn
Normal file
79
showcase_games/circuit_logic/scenes/level_select.tscn
Normal file
|
|
@ -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
|
||||
97
showcase_games/circuit_logic/scenes/main_menu.tscn
Normal file
97
showcase_games/circuit_logic/scenes/main_menu.tscn
Normal file
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
208
showcase_games/circuit_logic/scripts/autoload/puzzle_manager.gd
Normal file
208
showcase_games/circuit_logic/scripts/autoload/puzzle_manager.gd
Normal file
|
|
@ -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
|
||||
310
showcase_games/circuit_logic/scripts/game/game_controller.gd
Normal file
310
showcase_games/circuit_logic/scripts/game/game_controller.gd
Normal file
|
|
@ -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()
|
||||
112
showcase_games/circuit_logic/scripts/ui/level_select.gd
Normal file
112
showcase_games/circuit_logic/scripts/ui/level_select.gd
Normal file
|
|
@ -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")
|
||||
78
showcase_games/circuit_logic/scripts/ui/main_menu.gd
Normal file
78
showcase_games/circuit_logic/scripts/ui/main_menu.gd
Normal file
|
|
@ -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()
|
||||
13
showcase_games/neon_runner/icon.svg
Normal file
13
showcase_games/neon_runner/icon.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<linearGradient id="neon_grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00ffff"/>
|
||||
<stop offset="100%" style="stop-color:#ff1493"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="128" height="128" fill="#0d0221"/>
|
||||
<path d="M20 100 L40 60 L50 80 L70 40 L90 70 L108 30" stroke="url(#neon_grad)" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||
<circle cx="108" cy="30" r="8" fill="#00ffff"/>
|
||||
<rect x="15" y="105" width="98" height="8" fill="#ff1493"/>
|
||||
<text x="64" y="125" font-family="Arial" font-size="12" fill="#00ffff" text-anchor="middle">NEON</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 694 B |
61
showcase_games/neon_runner/project.godot
Normal file
61
showcase_games/neon_runner/project.godot
Normal file
|
|
@ -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)
|
||||
222
showcase_games/neon_runner/scenes/game.tscn
Normal file
222
showcase_games/neon_runner/scenes/game.tscn
Normal file
|
|
@ -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)
|
||||
100
showcase_games/neon_runner/scenes/main_menu.tscn
Normal file
100
showcase_games/neon_runner/scenes/main_menu.tscn
Normal file
|
|
@ -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
|
||||
79
showcase_games/neon_runner/scenes/player.tscn
Normal file
79
showcase_games/neon_runner/scenes/player.tscn
Normal file
|
|
@ -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)
|
||||
66
showcase_games/neon_runner/scripts/autoload/audio_manager.gd
Normal file
66
showcase_games/neon_runner/scripts/autoload/audio_manager.gd
Normal file
|
|
@ -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)
|
||||
89
showcase_games/neon_runner/scripts/autoload/game_manager.gd
Normal file
89
showcase_games/neon_runner/scripts/autoload/game_manager.gd
Normal file
|
|
@ -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()
|
||||
33
showcase_games/neon_runner/scripts/game/game_controller.gd
Normal file
33
showcase_games/neon_runner/scripts/game/game_controller.gd
Normal file
|
|
@ -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")
|
||||
272
showcase_games/neon_runner/scripts/game/level_generator.gd
Normal file
272
showcase_games/neon_runner/scripts/game/level_generator.gd
Normal file
|
|
@ -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
|
||||
149
showcase_games/neon_runner/scripts/game/player.gd
Normal file
149
showcase_games/neon_runner/scripts/game/player.gd
Normal file
|
|
@ -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
|
||||
98
showcase_games/neon_runner/scripts/ui/game_ui.gd
Normal file
98
showcase_games/neon_runner/scripts/ui/game_ui.gd
Normal file
|
|
@ -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()
|
||||
40
showcase_games/neon_runner/scripts/ui/main_menu.gd
Normal file
40
showcase_games/neon_runner/scripts/ui/main_menu.gd
Normal file
|
|
@ -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
|
||||
28
showcase_games/void_explorer/icon.svg
Normal file
28
showcase_games/void_explorer/icon.svg
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<radialGradient id="void_grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" style="stop-color:#1a1a2e"/>
|
||||
<stop offset="100%" style="stop-color:#000008"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="128" height="128" fill="url(#void_grad)"/>
|
||||
<!-- Stars -->
|
||||
<circle cx="20" cy="30" r="1" fill="white" opacity="0.8"/>
|
||||
<circle cx="100" cy="20" r="1.5" fill="white" opacity="0.6"/>
|
||||
<circle cx="45" cy="15" r="1" fill="white" opacity="0.9"/>
|
||||
<circle cx="80" cy="50" r="1" fill="white" opacity="0.7"/>
|
||||
<circle cx="110" cy="80" r="1" fill="white" opacity="0.5"/>
|
||||
<circle cx="15" cy="90" r="1.5" fill="white" opacity="0.8"/>
|
||||
<!-- Station silhouette -->
|
||||
<rect x="30" y="55" width="68" height="25" rx="3" fill="#2a2a4a"/>
|
||||
<rect x="45" y="45" width="38" height="40" rx="2" fill="#3a3a5a"/>
|
||||
<rect x="55" y="40" width="18" height="50" rx="2" fill="#4a4a6a"/>
|
||||
<!-- Windows -->
|
||||
<rect x="48" y="50" width="6" height="6" fill="#4a8fff"/>
|
||||
<rect x="58" y="50" width="6" height="6" fill="#4a8fff"/>
|
||||
<rect x="68" y="50" width="6" height="6" fill="#ff4a4a"/>
|
||||
<!-- Antenna -->
|
||||
<line x1="64" y1="40" x2="64" y2="25" stroke="#6a6a8a" stroke-width="2"/>
|
||||
<circle cx="64" cy="23" r="3" fill="#ff4a4a"/>
|
||||
<text x="64" y="115" font-family="Arial" font-size="10" fill="#4a8fff" text-anchor="middle">VOID</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
96
showcase_games/void_explorer/project.godot
Normal file
96
showcase_games/void_explorer/project.godot
Normal file
|
|
@ -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
|
||||
101
showcase_games/void_explorer/scenes/main_menu.tscn
Normal file
101
showcase_games/void_explorer/scenes/main_menu.tscn
Normal file
|
|
@ -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
|
||||
46
showcase_games/void_explorer/scenes/player.tscn
Normal file
46
showcase_games/void_explorer/scenes/player.tscn
Normal file
|
|
@ -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
|
||||
188
showcase_games/void_explorer/scenes/station.tscn
Normal file
188
showcase_games/void_explorer/scenes/station.tscn
Normal file
|
|
@ -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)
|
||||
101
showcase_games/void_explorer/scripts/autoload/game_state.gd
Normal file
101
showcase_games/void_explorer/scripts/autoload/game_state.gd
Normal file
|
|
@ -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()
|
||||
|
|
@ -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]
|
||||
149
showcase_games/void_explorer/scripts/player/player_controller.gd
Normal file
149
showcase_games/void_explorer/scripts/player/player_controller.gd
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
68
showcase_games/void_explorer/scripts/ui/game_ui.gd
Normal file
68
showcase_games/void_explorer/scripts/ui/game_ui.gd
Normal file
|
|
@ -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)
|
||||
63
showcase_games/void_explorer/scripts/ui/main_menu.gd
Normal file
63
showcase_games/void_explorer/scripts/ui/main_menu.gd
Normal file
|
|
@ -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()
|
||||
Loading…
Reference in a new issue