Added tetris-game. /JL
This commit is contained in:
4
tetris-game/.editorconfig
Normal file
4
tetris-game/.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
2
tetris-game/.gitattributes
vendored
Normal file
2
tetris-game/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Normalize EOL for all files that Git considers text files.
|
||||||
|
* text=auto eol=lf
|
||||||
3
tetris-game/.gitignore
vendored
Normal file
3
tetris-game/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
1
tetris-game/icon.svg
Normal file
1
tetris-game/icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 994 B |
37
tetris-game/icon.svg.import
Normal file
37
tetris-game/icon.svg.import
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dck747bvw17v0"
|
||||||
|
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
44
tetris-game/project.godot
Normal file
44
tetris-game/project.godot
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
; 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="Tetris Game"
|
||||||
|
run/main_scene="uid://ckvfgxn4jyoi6"
|
||||||
|
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||||
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
move_left={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
move_right={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
soft_drop={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
rotate={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
7
tetris-game/scenes/board.tscn
Normal file
7
tetris-game/scenes/board.tscn
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://d0rk850ey8e0g"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://yc60e2spsmjh" path="res://scripts/board.gd" id="1_p0ybc"]
|
||||||
|
|
||||||
|
[node name="Board" type="Node2D"]
|
||||||
|
position = Vector2(100, 0)
|
||||||
|
script = ExtResource("1_p0ybc")
|
||||||
69
tetris-game/scenes/game_over.tscn
Normal file
69
tetris-game/scenes/game_over.tscn
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://dkspo8vx4qcvb"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dq8ifo7mfpflr" path="res://scripts/game_over.gd" id="1_xeevv"]
|
||||||
|
|
||||||
|
[node name="GameOver" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
scale = Vector2(2, 2)
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
script = ExtResource("1_xeevv")
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="CenterContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -67.0
|
||||||
|
offset_top = -69.5
|
||||||
|
offset_right = 67.0
|
||||||
|
offset_bottom = 69.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="LabelTitle" type="Label" parent="CenterContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
text = "GAME OVER"
|
||||||
|
|
||||||
|
[node name="LabelScore" type="Label" parent="CenterContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="LabelHighScore" type="Label" parent="CenterContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="LabelLines" type="Label" parent="CenterContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="ButtonAgain" type="Button" parent="CenterContainer/Panel/VBoxContainer/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Play Again"
|
||||||
|
|
||||||
|
[node name="ButtonQuit" type="Button" parent="CenterContainer/Panel/VBoxContainer/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Quit"
|
||||||
40
tetris-game/scenes/hud_left.tscn
Normal file
40
tetris-game/scenes/hud_left.tscn
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[gd_scene format=3 uid="uid://ds6jmtmpkvtxp"]
|
||||||
|
|
||||||
|
[node name="HUDLeft" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 4
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_top = -20.0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 20.0
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="LabelScoreTitle" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Score"
|
||||||
|
|
||||||
|
[node name="LabelScoreValue" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0"
|
||||||
|
|
||||||
|
[node name="LabelHighScoreTitle" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Highscore"
|
||||||
|
|
||||||
|
[node name="LabelHighScoreValue" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0"
|
||||||
|
|
||||||
|
[node name="LabelLinesTitle" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Lines"
|
||||||
|
|
||||||
|
[node name="LabelLinesValue" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0"
|
||||||
22
tetris-game/scenes/hud_right.tscn
Normal file
22
tetris-game/scenes/hud_right.tscn
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://dsc4ofx4h3gos"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://d0wlixgjpcnyc" path="res://scripts/next_preview.gd" id="1_hyjpj"]
|
||||||
|
|
||||||
|
[node name="HUDRight" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_left = 350.0
|
||||||
|
offset_right = 390.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="LabelNextTitle" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Next"
|
||||||
|
|
||||||
|
[node name="NextPreview" type="Node2D" parent="VBoxContainer"]
|
||||||
|
script = ExtResource("1_hyjpj")
|
||||||
51
tetris-game/scenes/main.tscn
Normal file
51
tetris-game/scenes/main.tscn
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
[gd_scene load_steps=7 format=3 uid="uid://ckvfgxn4jyoi6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ctadm2gu04xgn" path="res://scripts/main.gd" id="1_tbgi4"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://d0rk850ey8e0g" path="res://scenes/board.tscn" id="2_tefeu"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ds6jmtmpkvtxp" path="res://scenes/hud_left.tscn" id="3_o6xl0"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dsc4ofx4h3gos" path="res://scenes/hud_right.tscn" id="4_tipki"]
|
||||||
|
[ext_resource type="Shader" uid="uid://ct7v5mfajkvir" path="res://shaders/blur_overlay.gdshader" id="5_85g3d"]
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_lquwl"]
|
||||||
|
shader = ExtResource("5_85g3d")
|
||||||
|
shader_parameter/blur_radius = 8.00000038
|
||||||
|
shader_parameter/samples = 7
|
||||||
|
shader_parameter/blur_strength = 0.70000003325
|
||||||
|
shader_parameter/dim_amount = 0.150000007125
|
||||||
|
|
||||||
|
[node name="Main" type="Node2D"]
|
||||||
|
script = ExtResource("1_tbgi4")
|
||||||
|
|
||||||
|
[node name="GameRoot" type="Node2D" parent="."]
|
||||||
|
|
||||||
|
[node name="Board" parent="GameRoot" instance=ExtResource("2_tefeu")]
|
||||||
|
|
||||||
|
[node name="HUDLeft" parent="GameRoot" instance=ExtResource("3_o6xl0")]
|
||||||
|
offset_top = 150.0
|
||||||
|
offset_bottom = 190.0
|
||||||
|
|
||||||
|
[node name="HUDRight" parent="GameRoot" instance=ExtResource("4_tipki")]
|
||||||
|
offset_top = 24.0
|
||||||
|
offset_bottom = 64.0
|
||||||
|
|
||||||
|
[node name="UILayer" type="CanvasLayer" parent="GameRoot"]
|
||||||
|
|
||||||
|
[node name="UIRoot" type="Control" parent="GameRoot/UILayer"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_use_anchors_ = true
|
||||||
|
|
||||||
|
[node name="BlurOverlay" type="ColorRect" parent="GameRoot/UILayer/UIRoot"]
|
||||||
|
visible = false
|
||||||
|
material = SubResource("ShaderMaterial_lquwl")
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
276
tetris-game/scripts/board.gd
Normal file
276
tetris-game/scripts/board.gd
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const BOARD_WIDTH := 10
|
||||||
|
const BOARD_HEIGHT := 20
|
||||||
|
const CELL_SIZE := 24 # pixels
|
||||||
|
|
||||||
|
# Types of pieces
|
||||||
|
# Each shape is a list of Vector2i, relative to a pivot (0,0)
|
||||||
|
const SHAPES := {
|
||||||
|
"I": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(2,0)],
|
||||||
|
"O": [Vector2i(0,0), Vector2i(1,0), Vector2i(0,1), Vector2i(1,1)],
|
||||||
|
"T": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(0,1)],
|
||||||
|
"L": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(1,1)],
|
||||||
|
"J": [Vector2i(-1,1), Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0)],
|
||||||
|
"S": [Vector2i(0,0), Vector2i(1,0), Vector2i(-1,1), Vector2i(0,1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Link shape keys to colors
|
||||||
|
const COLORS := {
|
||||||
|
"I": Color(0.0, 1.0, 1.0),
|
||||||
|
"O": Color(1.0, 1.0, 0.0),
|
||||||
|
"T": Color(0.6, 0.0, 0.8),
|
||||||
|
"L": Color(1.0, 0.5, 0.0),
|
||||||
|
"J": Color(0.0, 0.0, 1.0),
|
||||||
|
"S": Color(0.0, 0.5, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2D grid: 0 = empty, else a String shape key
|
||||||
|
var grid: Array = []
|
||||||
|
|
||||||
|
# Current falling piece
|
||||||
|
var current_shape_key: String
|
||||||
|
var current_blocks: Array[Vector2i] = []
|
||||||
|
var current_pos: Vector2i
|
||||||
|
var current_color: Color
|
||||||
|
|
||||||
|
# Next piece
|
||||||
|
var next_shape_key: String = ""
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
|
||||||
|
# Drop timing
|
||||||
|
var drop_interval := 0.7
|
||||||
|
var drop_timer := 0.0
|
||||||
|
var is_game_over := false
|
||||||
|
|
||||||
|
# Signals to update HUD and preview
|
||||||
|
signal lines_cleared(total_lines: int, score: int, highscore: int)
|
||||||
|
signal next_piece_changed(shape_key: String, shapes: Dictionary, colors: Dictionary)
|
||||||
|
signal game_over(score: int, highscore: int, total_lines: int)
|
||||||
|
|
||||||
|
var score := 0
|
||||||
|
var highscore := 0
|
||||||
|
var total_lines_cleared := 0
|
||||||
|
|
||||||
|
func reset_game() -> void:
|
||||||
|
score = 0
|
||||||
|
total_lines_cleared = 0
|
||||||
|
drop_timer = 0.0
|
||||||
|
is_game_over = false
|
||||||
|
|
||||||
|
_init_grid()
|
||||||
|
_spawn_initial_pieces()
|
||||||
|
|
||||||
|
# Update HUD to show reset stats
|
||||||
|
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
rng.randomize()
|
||||||
|
_load_highscore()
|
||||||
|
reset_game()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if is_game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
drop_timer += delta
|
||||||
|
if drop_timer >= drop_interval:
|
||||||
|
drop_timer = 0.0
|
||||||
|
_try_move(Vector2i(0, 1), true)
|
||||||
|
|
||||||
|
_handle_input()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _init_grid() -> void:
|
||||||
|
grid.resize(BOARD_HEIGHT)
|
||||||
|
for y in range(BOARD_HEIGHT):
|
||||||
|
grid[y] = []
|
||||||
|
grid[y].resize(BOARD_WIDTH)
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
grid[y][x] = "" # empty string = no blocks
|
||||||
|
|
||||||
|
func _spawn_initial_pieces() -> void:
|
||||||
|
next_shape_key = _random_shape()
|
||||||
|
_spawn_new_piece()
|
||||||
|
|
||||||
|
func _random_shape() -> String:
|
||||||
|
var keys := SHAPES.keys()
|
||||||
|
return keys[rng.randi() % keys.size()]
|
||||||
|
|
||||||
|
func _spawn_new_piece() -> void:
|
||||||
|
# Move "next" to "current"
|
||||||
|
current_shape_key = next_shape_key
|
||||||
|
|
||||||
|
# Get the raw shape array (untyped)
|
||||||
|
var shape_array: Array = SHAPES[current_shape_key]
|
||||||
|
|
||||||
|
# Fill our typed Array[Vector2i]
|
||||||
|
current_blocks.clear()
|
||||||
|
for p in shape_array:
|
||||||
|
current_blocks.append(p as Vector2i)
|
||||||
|
|
||||||
|
# Color for this piece
|
||||||
|
current_color = Color.WHITE
|
||||||
|
if COLORS.has(current_shape_key):
|
||||||
|
current_color = COLORS[current_shape_key] as Color
|
||||||
|
|
||||||
|
# Spawn near top center
|
||||||
|
current_pos = Vector2i(BOARD_WIDTH / 2, 1)
|
||||||
|
|
||||||
|
# Choose the next upcoming piece
|
||||||
|
next_shape_key = _random_shape()
|
||||||
|
emit_signal("next_piece_changed", next_shape_key, SHAPES, COLORS)
|
||||||
|
|
||||||
|
# Check for immediate collision → game over
|
||||||
|
if _is_colliding(current_pos, current_blocks):
|
||||||
|
_game_over()
|
||||||
|
|
||||||
|
func _handle_input() -> void:
|
||||||
|
if is_game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("move_left"):
|
||||||
|
_try_move(Vector2i(-1, 0))
|
||||||
|
if Input.is_action_just_pressed("move_right"):
|
||||||
|
_try_move(Vector2i(1, 0))
|
||||||
|
if Input.is_action_pressed("soft_drop"):
|
||||||
|
_try_move(Vector2i(0, 1))
|
||||||
|
if Input.is_action_just_pressed("rotate"):
|
||||||
|
_try_rotate()
|
||||||
|
|
||||||
|
func _try_move(offset: Vector2i, lock_on_fail: bool = false) -> void:
|
||||||
|
var new_pos := current_pos + offset
|
||||||
|
if not _is_colliding(new_pos, current_blocks):
|
||||||
|
current_pos = new_pos
|
||||||
|
elif lock_on_fail and offset.y > 0:
|
||||||
|
_lock_piece()
|
||||||
|
|
||||||
|
func _try_rotate() -> void:
|
||||||
|
var rotated: Array[Vector2i] = []
|
||||||
|
for b in current_blocks:
|
||||||
|
# (x, y) → (-y, x)
|
||||||
|
rotated.append(Vector2i(-b.y, b.x))
|
||||||
|
|
||||||
|
if not _is_colliding(current_pos, rotated):
|
||||||
|
current_blocks = rotated
|
||||||
|
|
||||||
|
func _is_colliding(pos: Vector2i, blocks: Array[Vector2i]) -> bool:
|
||||||
|
for b in blocks:
|
||||||
|
var x := pos.x + b.x
|
||||||
|
var y := pos.y + b.y
|
||||||
|
if x < 0 or x >= BOARD_WIDTH or y < 0 or y >= BOARD_HEIGHT:
|
||||||
|
return true
|
||||||
|
if grid[y][x] != "":
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _lock_piece() -> void:
|
||||||
|
# Transfer current piece into grid
|
||||||
|
var points: int
|
||||||
|
for b in current_blocks:
|
||||||
|
var x := current_pos.x + b.x
|
||||||
|
var y := current_pos.y + b.y
|
||||||
|
if y >= 0 and y < BOARD_HEIGHT and x >= 0 and x < BOARD_WIDTH:
|
||||||
|
grid[y][x] = current_shape_key
|
||||||
|
|
||||||
|
var cleared := _clear_full_lines()
|
||||||
|
|
||||||
|
# scoring (simple rules)
|
||||||
|
if cleared > 0:
|
||||||
|
total_lines_cleared += cleared
|
||||||
|
match cleared:
|
||||||
|
1:
|
||||||
|
points = 100
|
||||||
|
2:
|
||||||
|
points = 300
|
||||||
|
3:
|
||||||
|
points = 500
|
||||||
|
4:
|
||||||
|
points = 800
|
||||||
|
_:
|
||||||
|
points = cleared * 200
|
||||||
|
score += points
|
||||||
|
if score > highscore:
|
||||||
|
highscore = score
|
||||||
|
_save_highscore()
|
||||||
|
|
||||||
|
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
|
||||||
|
|
||||||
|
_spawn_new_piece()
|
||||||
|
|
||||||
|
func _clear_full_lines() -> int:
|
||||||
|
var cleared_lines := 0
|
||||||
|
for y in range(BOARD_HEIGHT - 1, -1, -1):
|
||||||
|
var is_full := true
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
if grid[y][x] == "":
|
||||||
|
is_full = false
|
||||||
|
break
|
||||||
|
if is_full:
|
||||||
|
_drop_lines_above(y)
|
||||||
|
cleared_lines += 1
|
||||||
|
y += 1 # re-check same row index (now replaced)
|
||||||
|
return cleared_lines
|
||||||
|
|
||||||
|
func _drop_lines_above(row: int) -> void:
|
||||||
|
for y in range(row, 0, -1):
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
grid[y][x] = grid[y - 1][x]
|
||||||
|
# top row becomes empty
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
grid[0][x] = ""
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
# Draw background grid (optional)
|
||||||
|
for y in range(BOARD_HEIGHT):
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
var rect := Rect2(
|
||||||
|
Vector2(x * CELL_SIZE, y * CELL_SIZE),
|
||||||
|
Vector2(CELL_SIZE, CELL_SIZE)
|
||||||
|
)
|
||||||
|
draw_rect(rect, Color(0.05, 0.05, 0.05), false, 1.0)
|
||||||
|
|
||||||
|
# Draw placed blocks
|
||||||
|
for y in range(BOARD_HEIGHT):
|
||||||
|
for x in range(BOARD_WIDTH):
|
||||||
|
var key: String = grid[y][x]
|
||||||
|
if key != "":
|
||||||
|
var color: Color = Color.WHITE
|
||||||
|
if COLORS.has(key):
|
||||||
|
color = COLORS[key] as Color
|
||||||
|
_draw_cell(x, y, color)
|
||||||
|
|
||||||
|
# Draw current falling piece
|
||||||
|
for b in current_blocks:
|
||||||
|
var x := current_pos.x + b.x
|
||||||
|
var y := current_pos.y + b.y
|
||||||
|
_draw_cell(x, y, current_color)
|
||||||
|
|
||||||
|
func _draw_cell(x: int, y: int, color: Color) -> void:
|
||||||
|
var pos := Vector2(x * CELL_SIZE, y * CELL_SIZE)
|
||||||
|
var rect := Rect2(pos, Vector2(CELL_SIZE, CELL_SIZE))
|
||||||
|
draw_rect(rect.grow(-1), color, true) # small border
|
||||||
|
|
||||||
|
func _game_over() -> void:
|
||||||
|
is_game_over = true
|
||||||
|
|
||||||
|
# Make sure HUD shows final values
|
||||||
|
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
|
||||||
|
|
||||||
|
# Inform main scene that the game ended
|
||||||
|
emit_signal("game_over", score, highscore, total_lines_cleared)
|
||||||
|
|
||||||
|
func _save_highscore() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value("stats", "highscore", highscore)
|
||||||
|
cfg.save("user://tetris.cfg")
|
||||||
|
|
||||||
|
func _load_highscore() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
var err := cfg.load("user://tetris.cfg")
|
||||||
|
if err == OK:
|
||||||
|
highscore = int(cfg.get_value("stats", "highscore", 0))
|
||||||
|
else:
|
||||||
|
highscore = 0
|
||||||
|
|
||||||
1
tetris-game/scripts/board.gd.uid
Normal file
1
tetris-game/scripts/board.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://yc60e2spsmjh
|
||||||
34
tetris-game/scripts/game_over.gd
Normal file
34
tetris-game/scripts/game_over.gd
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@export var final_score: int = 0
|
||||||
|
@export var final_highscore: int = 0
|
||||||
|
@export var total_lines: int = 0
|
||||||
|
|
||||||
|
@onready var score_label: Label = $CenterContainer/Panel/VBoxContainer/LabelScore
|
||||||
|
@onready var highscore_label: Label = $CenterContainer/Panel/VBoxContainer/LabelHighScore
|
||||||
|
@onready var lines_label: Label = $CenterContainer/Panel/VBoxContainer/LabelLines
|
||||||
|
@onready var retry_button: Button = $CenterContainer/Panel/VBoxContainer/HBoxContainer/ButtonAgain
|
||||||
|
@onready var quit_button: Button = $CenterContainer/Panel/VBoxContainer/HBoxContainer/ButtonQuit
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_update_labels()
|
||||||
|
|
||||||
|
retry_button.pressed.connect(_on_retry_pressed)
|
||||||
|
quit_button.pressed.connect(_on_quit_pressed)
|
||||||
|
|
||||||
|
func _update_labels() -> void:
|
||||||
|
if score_label:
|
||||||
|
score_label.text = "Score: %d" % final_score
|
||||||
|
if highscore_label:
|
||||||
|
highscore_label.text = "Highscore: %d" % final_highscore
|
||||||
|
if lines_label:
|
||||||
|
lines_label.text = "Lines: %d" % total_lines
|
||||||
|
|
||||||
|
func _on_retry_pressed() -> void:
|
||||||
|
var main := get_tree().current_scene
|
||||||
|
if main and main.has_method("reset_game"):
|
||||||
|
main.reset_game()
|
||||||
|
queue_free() # remove overlay
|
||||||
|
|
||||||
|
func _on_quit_pressed() -> void:
|
||||||
|
get_tree().quit()
|
||||||
1
tetris-game/scripts/game_over.gd.uid
Normal file
1
tetris-game/scripts/game_over.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dq8ifo7mfpflr
|
||||||
83
tetris-game/scripts/main.gd
Normal file
83
tetris-game/scripts/main.gd
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const GAME_OVER_SCENE := preload("res://scenes/game_over.tscn")
|
||||||
|
|
||||||
|
@onready var game_root: Node2D = $GameRoot
|
||||||
|
@onready var board: Node2D = $GameRoot/Board
|
||||||
|
@onready var hud_left: Control = $GameRoot/HUDLeft
|
||||||
|
@onready var hud_right: Control = $GameRoot/HUDRight
|
||||||
|
@onready var ui_root: Control = $GameRoot/UILayer/UIRoot
|
||||||
|
@onready var blur_overlay: ColorRect = $GameRoot/UILayer/UIRoot/BlurOverlay # New node
|
||||||
|
|
||||||
|
@onready var label_score_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelScoreValue
|
||||||
|
@onready var label_highscore_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelHighScoreValue
|
||||||
|
@onready var label_lines_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelLinesValue
|
||||||
|
@onready var next_preview: Node2D = $GameRoot/HUDRight/VBoxContainer/NextPreview
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
board.connect("lines_cleared", Callable(self, "_on_lines_cleared"))
|
||||||
|
board.connect("next_piece_changed", Callable(self, "_on_next_piece_changed"))
|
||||||
|
board.connect("game_over", Callable(self, "_on_game_over"))
|
||||||
|
|
||||||
|
# Center once and whenever the window size changes
|
||||||
|
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
||||||
|
_on_viewport_size_changed()
|
||||||
|
|
||||||
|
func _on_viewport_size_changed() -> void:
|
||||||
|
var window_size: Vector2 = get_viewport_rect().size
|
||||||
|
var layout_bounds := _get_layout_bounds()
|
||||||
|
|
||||||
|
var layout_center := layout_bounds.position + layout_bounds.size * 0.5
|
||||||
|
var target_center := window_size * 0.5
|
||||||
|
|
||||||
|
# Move the whole game so the *layout* center matches the window center
|
||||||
|
game_root.position = target_center - layout_center
|
||||||
|
|
||||||
|
func _on_lines_cleared(total_lines: int, score: int, highscore: int) -> void:
|
||||||
|
label_score_value.text = str(score)
|
||||||
|
label_highscore_value.text = str(highscore)
|
||||||
|
label_lines_value.text = str(total_lines)
|
||||||
|
|
||||||
|
func _get_layout_bounds() -> Rect2:
|
||||||
|
# Board rectangle (we know its size from constants)
|
||||||
|
var board_size := Vector2(
|
||||||
|
board.BOARD_WIDTH * board.CELL_SIZE,
|
||||||
|
board.BOARD_HEIGHT * board.CELL_SIZE
|
||||||
|
)
|
||||||
|
var bounds := Rect2(board.position, board_size)
|
||||||
|
|
||||||
|
# Merge HUDLeft and HUDRight rectangles into the bounds
|
||||||
|
if hud_left:
|
||||||
|
bounds = bounds.merge(hud_left.get_rect())
|
||||||
|
if hud_right:
|
||||||
|
bounds = bounds.merge(hud_right.get_rect())
|
||||||
|
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
func _on_next_piece_changed(shape_key: String, shapes: Dictionary, colors: Dictionary) -> void:
|
||||||
|
next_preview.update_preview(shape_key, shapes, colors)
|
||||||
|
|
||||||
|
func _on_game_over(score: int, highscore: int, total_lines: int) -> void:
|
||||||
|
# 1) Turn on blur behind the overlay
|
||||||
|
if blur_overlay:
|
||||||
|
blur_overlay.visible = true
|
||||||
|
|
||||||
|
# 2) Show the GameOver UI on top
|
||||||
|
var game_over_layer := GAME_OVER_SCENE.instantiate()
|
||||||
|
game_over_layer.set_anchors_preset(Control.PRESET_CENTER) # New
|
||||||
|
game_over_layer.scale = Vector2(2, 2) # New
|
||||||
|
|
||||||
|
game_over_layer.final_score = score
|
||||||
|
game_over_layer.final_highscore = highscore
|
||||||
|
game_over_layer.total_lines = total_lines
|
||||||
|
|
||||||
|
ui_root.add_child(game_over_layer)
|
||||||
|
game_over_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
|
||||||
|
func reset_game() -> void:
|
||||||
|
# Hide blur again
|
||||||
|
if blur_overlay:
|
||||||
|
blur_overlay.visible = false
|
||||||
|
|
||||||
|
if board.has_method("reset_game"):
|
||||||
|
board.reset_game()
|
||||||
1
tetris-game/scripts/main.gd.uid
Normal file
1
tetris-game/scripts/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ctadm2gu04xgn
|
||||||
59
tetris-game/scripts/next_preview.gd
Normal file
59
tetris-game/scripts/next_preview.gd
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const CELL_SIZE := 16
|
||||||
|
|
||||||
|
var shape_key: String = ""
|
||||||
|
var shapes: Dictionary = {}
|
||||||
|
var colors: Dictionary = {}
|
||||||
|
|
||||||
|
func update_preview(new_shape_key: String, new_shapes: Dictionary, new_colors: Dictionary) -> void:
|
||||||
|
shape_key = new_shape_key
|
||||||
|
shapes = new_shapes
|
||||||
|
colors = new_colors
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
# Debug frame so we can see that the node exists
|
||||||
|
var debug_rect := Rect2(Vector2.ZERO, Vector2(80, 80))
|
||||||
|
draw_rect(debug_rect, Color(0.2, 0.2, 0.2, 1.0), false, 2.0)
|
||||||
|
|
||||||
|
if shape_key == "" or not shapes.has(shape_key):
|
||||||
|
return
|
||||||
|
|
||||||
|
var blocks: Array = shapes[shape_key]
|
||||||
|
|
||||||
|
# Find bounding box of the shape
|
||||||
|
var min_x := 999
|
||||||
|
var max_x := -999
|
||||||
|
var min_y := 999
|
||||||
|
var max_y := -999
|
||||||
|
|
||||||
|
for b in blocks:
|
||||||
|
var v: Vector2i = b
|
||||||
|
if v.x < min_x:
|
||||||
|
min_x = v.x
|
||||||
|
if v.x > max_x:
|
||||||
|
max_x = v.x
|
||||||
|
if v.y < min_y:
|
||||||
|
min_y = v.y
|
||||||
|
if v.y > max_y:
|
||||||
|
max_y = v.y
|
||||||
|
|
||||||
|
var width := (max_x - min_x + 1) * CELL_SIZE
|
||||||
|
var height := (max_y - min_y + 1) * CELL_SIZE
|
||||||
|
|
||||||
|
# Center the shape in our local 80x80 area
|
||||||
|
var total_size := Vector2(width, height)
|
||||||
|
var preview_center := Vector2(40, 40) # middle of debug_rect
|
||||||
|
var offset := preview_center - total_size * 0.5
|
||||||
|
|
||||||
|
var color: Color = Color.WHITE
|
||||||
|
if colors.has(shape_key):
|
||||||
|
color = colors[shape_key] as Color
|
||||||
|
|
||||||
|
for b in blocks:
|
||||||
|
var v: Vector2i = b
|
||||||
|
var x := (v.x - min_x) * CELL_SIZE
|
||||||
|
var y := (v.y - min_y) * CELL_SIZE
|
||||||
|
var rect := Rect2(offset + Vector2(x, y), Vector2(CELL_SIZE, CELL_SIZE))
|
||||||
|
draw_rect(rect.grow(-1), color, true)
|
||||||
1
tetris-game/scripts/next_preview.gd.uid
Normal file
1
tetris-game/scripts/next_preview.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d0wlixgjpcnyc
|
||||||
39
tetris-game/shaders/blur_overlay.gdshader
Normal file
39
tetris-game/shaders/blur_overlay.gdshader
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
|
||||||
|
|
||||||
|
// Radius in pixels
|
||||||
|
uniform float blur_radius : hint_range(0.0, 32.0) = 8.0;
|
||||||
|
// Number of samples in each direction
|
||||||
|
uniform int samples : hint_range(1, 20) = 6;
|
||||||
|
// 0 = no blur, 1 = fully blurred
|
||||||
|
uniform float blur_strength : hint_range(0.0, 1.0) = 0.6;
|
||||||
|
// Optional slight dimming of background
|
||||||
|
uniform float dim_amount : hint_range(0.0, 0.5) = 0.1;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 original = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
|
||||||
|
|
||||||
|
vec2 pixel = SCREEN_PIXEL_SIZE * blur_radius;
|
||||||
|
|
||||||
|
vec4 blurred = vec4(0.0);
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int x = -samples; x <= samples; x++) {
|
||||||
|
for (int y = -samples; y <= samples; y++) {
|
||||||
|
vec2 offset = vec2(float(x), float(y)) * pixel;
|
||||||
|
blurred += textureLod(SCREEN_TEXTURE, SCREEN_UV + offset, 0.0);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blurred /= float(count);
|
||||||
|
|
||||||
|
// Blend between original and blurred
|
||||||
|
vec4 color = mix(original, blurred, blur_strength);
|
||||||
|
|
||||||
|
// Slight dimming, if wanted
|
||||||
|
color.rgb *= (1.0 - dim_amount);
|
||||||
|
|
||||||
|
COLOR = color;
|
||||||
|
}
|
||||||
1
tetris-game/shaders/blur_overlay.gdshader.uid
Normal file
1
tetris-game/shaders/blur_overlay.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ct7v5mfajkvir
|
||||||
Reference in New Issue
Block a user