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