Files
2025-09-15 19:10:37 +08:00

658 lines
18 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Panel
# 游戏常量
const BOARD_WIDTH = 10
const BOARD_HEIGHT = 20
const CELL_SIZE = 30
const DATA_FILE_PATH = "user://playergamedata.json"
# 特殊方块类型
enum SpecialType {
NORMAL,
BOMB, # 炸弹方块,消除周围方块
LINE, # 直线方块,消除整行
RAINBOW # 彩虹方块,消除同色方块
}
# 方块类型
enum PieceType {
I, O, T, S, Z, J, L
}
# 方块形状定义
const PIECE_SHAPES = {
PieceType.I: [
[[1, 1, 1, 1]],
[[1], [1], [1], [1]]
],
PieceType.O: [
[[1, 1], [1, 1]]
],
PieceType.T: [
[[0, 1, 0], [1, 1, 1]],
[[1, 0], [1, 1], [1, 0]],
[[1, 1, 1], [0, 1, 0]],
[[0, 1], [1, 1], [0, 1]]
],
PieceType.S: [
[[0, 1, 1], [1, 1, 0]],
[[1, 0], [1, 1], [0, 1]]
],
PieceType.Z: [
[[1, 1, 0], [0, 1, 1]],
[[0, 1], [1, 1], [1, 0]]
],
PieceType.J: [
[[1, 0, 0], [1, 1, 1]],
[[1, 1], [1, 0], [1, 0]],
[[1, 1, 1], [0, 0, 1]],
[[0, 1], [0, 1], [1, 1]]
],
PieceType.L: [
[[0, 0, 1], [1, 1, 1]],
[[1, 0], [1, 0], [1, 1]],
[[1, 1, 1], [1, 0, 0]],
[[1, 1], [0, 1], [0, 1]]
]
}
# 方块颜色
const PIECE_COLORS = {
PieceType.I: Color.CYAN,
PieceType.O: Color.YELLOW,
PieceType.T: Color.MAGENTA,
PieceType.S: Color.GREEN,
PieceType.Z: Color.RED,
PieceType.J: Color.BLUE,
PieceType.L: Color.ORANGE
}
# 游戏变量
var board = []
var current_piece = null
var current_piece_pos = Vector2()
var current_piece_rotation = 0
var next_piece_type = PieceType.I
var score = 0
var level = 1
var lines_cleared = 0
var game_over = false
var game_started = false # 添加游戏开始状态
var drop_time = 1.0
var best_score = 0
var games_played = 0
var total_lines_cleared = 0
var combo_count = 0
var special_pieces = [] # 存储特殊方块位置和类型
var player_data = {}
# 节点引用
@onready var game_area = $GameArea
@onready var next_piece_area = $NextPieceArea
@onready var score_label = $ScoreLabel
@onready var level_label = $LevelLabel
@onready var lines_label = $LinesLabel
@onready var game_over_label = $GameOverLabel
@onready var drop_timer = $DropTimer
@onready var virtual_controls = $VirtualControls
func _ready():
# 连接定时器信号
drop_timer.timeout.connect(_on_drop_timer_timeout)
# 设置游戏区域样式
game_area.modulate = Color(0.08, 0.12, 0.18, 0.9)
next_piece_area.modulate = Color(0.1, 0.15, 0.25, 0.9)
# 设置虚拟控制
setup_virtual_controls()
# 加载玩家数据
load_player_data()
# 显示游戏开始界面
show_start_screen()
func init_game():
# 重置游戏状态
game_over = false
game_started = true
score = 0
level = 1
lines_cleared = 0
drop_time = 1.0
combo_count = 0
games_played += 1
# 初始化游戏板
board.clear()
special_pieces.clear()
for y in range(BOARD_HEIGHT):
var row = []
for x in range(BOARD_WIDTH):
row.append(0)
board.append(row)
# 生成第一个方块
next_piece_type = randi() % PieceType.size()
spawn_new_piece()
# 更新UI
update_ui()
game_over_label.visible = false
# 启动定时器
drop_timer.wait_time = drop_time
drop_timer.start()
func _input(event):
if event is InputEventKey and event.pressed:
# 游戏未开始时按Q键开始游戏
if not game_started:
if event.keycode == KEY_Q:
init_game()
return
if game_over:
if event.keycode == KEY_SPACE:
init_game()
elif event.keycode == KEY_Q:
init_game()
return
if not current_piece:
return
# 控制方块
match event.keycode:
KEY_A:
handle_move_left()
KEY_D:
handle_move_right()
KEY_S:
handle_move_down()
KEY_W:
handle_rotate()
KEY_SPACE:
handle_drop()
# 控制函数
func handle_move_left():
move_piece(-1, 0)
func handle_move_right():
move_piece(1, 0)
func handle_move_down():
move_piece(0, 1)
func handle_rotate():
rotate_piece()
func handle_drop():
drop_piece()
func handle_restart():
init_game()
func spawn_new_piece():
current_piece = {
"type": next_piece_type,
"rotation": 0
}
current_piece_pos = Vector2(BOARD_WIDTH / 2 - 1, 0)
current_piece_rotation = 0
# 生成下一个方块
next_piece_type = randi() % PieceType.size()
# 检查游戏是否结束
if not can_place_piece(current_piece_pos, current_piece_rotation):
game_over = true
show_game_over()
func move_piece(dx: int, dy: int):
var new_pos = current_piece_pos + Vector2(dx, dy)
if can_place_piece(new_pos, current_piece_rotation):
current_piece_pos = new_pos
queue_redraw()
func rotate_piece():
var new_rotation = (current_piece_rotation + 1) % get_piece_rotations(current_piece.type)
if can_place_piece(current_piece_pos, new_rotation):
current_piece_rotation = new_rotation
queue_redraw()
func drop_piece():
while can_place_piece(current_piece_pos + Vector2(0, 1), current_piece_rotation):
current_piece_pos.y += 1
place_piece()
func can_place_piece(pos: Vector2, rotation: int) -> bool:
var shape = get_piece_shape(current_piece.type, rotation)
for y in range(shape.size()):
for x in range(shape[y].size()):
if shape[y][x] == 1:
var board_x = pos.x + x
var board_y = pos.y + y
# 检查边界
if board_x < 0 or board_x >= BOARD_WIDTH or board_y >= BOARD_HEIGHT:
return false
# 检查碰撞
if board_y >= 0 and board[board_y][board_x] != 0:
return false
return true
func place_piece():
var shape = get_piece_shape(current_piece.type, current_piece_rotation)
for y in range(shape.size()):
for x in range(shape[y].size()):
if shape[y][x] == 1:
var board_x = current_piece_pos.x + x
var board_y = current_piece_pos.y + y
if board_y >= 0:
board[board_y][board_x] = current_piece.type + 1
# 检查并清除完整的行
clear_lines()
# 生成新方块
spawn_new_piece()
queue_redraw()
func clear_lines():
var lines_to_clear = []
# 找到完整的行
for y in range(BOARD_HEIGHT):
var is_full = true
for x in range(BOARD_WIDTH):
if board[y][x] == 0:
is_full = false
break
if is_full:
lines_to_clear.append(y)
# 处理特殊方块效果
process_special_pieces(lines_to_clear)
# 清除行并下移
for line_y in lines_to_clear:
board.erase(board[line_y])
var new_row = []
for x in range(BOARD_WIDTH):
new_row.append(0)
board.insert(0, new_row)
# 更新分数和等级
if lines_to_clear.size() > 0:
# 连击系统
combo_count += 1
var combo_bonus = combo_count * 50
lines_cleared += lines_to_clear.size()
total_lines_cleared += lines_to_clear.size()
# 计算分数(包含连击奖励)
var base_score = lines_to_clear.size() * 100 * level
var line_bonus = 0
match lines_to_clear.size():
1: line_bonus = 0
2: line_bonus = 300
3: line_bonus = 500
4: line_bonus = 800 # 俄罗斯方块
score += base_score + line_bonus + combo_bonus
# 每10行提升一个等级速度递增
level = lines_cleared / 10 + 1
drop_time = max(0.05, 1.0 - (level - 1) * 0.08)
drop_timer.wait_time = drop_time
# 随机生成特殊方块
if randf() < 0.1 + level * 0.02: # 等级越高,特殊方块概率越大
generate_special_piece()
else:
# 重置连击
combo_count = 0
update_ui()
func get_piece_shape(type: PieceType, rotation: int) -> Array:
return PIECE_SHAPES[type][rotation]
func get_piece_rotations(type: PieceType) -> int:
return PIECE_SHAPES[type].size()
func update_ui():
score_label.text = "🏆 分数: " + str(score) + "\n💎 最高: " + str(best_score)
level_label.text = "⚡ 等级: " + str(level) + "\n🎮 游戏: " + str(games_played)
lines_label.text = "📊 消除: " + str(lines_cleared) + "\n🔥 连击: " + str(combo_count)
func show_game_over():
drop_timer.stop()
game_started = false
# 检查并更新最高分
if score > best_score:
best_score = score
# 保存玩家数据
save_player_data()
# 显示游戏结束信息
game_over_label.text = "🎮 游戏结束 🎮\n\n🏆 本次分数: " + str(score) + "\n💎 最高分数: " + str(best_score) + "\n📊 消除行数: " + str(lines_cleared) + "\n⚡ 达到等级: " + str(level) + "\n\n🔄 按Q键或空格重新开始"
game_over_label.visible = true
func _on_drop_timer_timeout():
if not game_started or game_over or not current_piece:
return
if can_place_piece(current_piece_pos + Vector2(0, 1), current_piece_rotation):
current_piece_pos.y += 1
queue_redraw()
else:
place_piece()
# 虚拟控制设置
func setup_virtual_controls():
if virtual_controls:
virtual_controls.get_node("LeftButton").pressed.connect(_on_virtual_button_pressed.bind("left"))
virtual_controls.get_node("RightButton").pressed.connect(_on_virtual_button_pressed.bind("right"))
virtual_controls.get_node("DownButton").pressed.connect(_on_virtual_button_pressed.bind("down"))
virtual_controls.get_node("RotateButton").pressed.connect(_on_virtual_button_pressed.bind("rotate"))
virtual_controls.get_node("DropButton").pressed.connect(_on_virtual_button_pressed.bind("drop"))
virtual_controls.get_node("RestartButton").pressed.connect(_on_virtual_button_pressed.bind("restart"))
func _on_virtual_button_pressed(action: String):
if not game_started:
if action == "restart":
init_game()
return
if game_over and action == "restart":
handle_restart()
return
if game_over or not current_piece:
return
match action:
"left":
handle_move_left()
"right":
handle_move_right()
"down":
handle_move_down()
"rotate":
handle_rotate()
"drop":
handle_drop()
# 特殊方块处理
func process_special_pieces(lines_to_clear: Array):
# 处理特殊方块效果
for special in special_pieces:
var pos = special.position
var type = special.type
match type:
SpecialType.BOMB:
# 炸弹方块清除周围3x3区域
for dy in range(-1, 2):
for dx in range(-1, 2):
var x = pos.x + dx
var y = pos.y + dy
if x >= 0 and x < BOARD_WIDTH and y >= 0 and y < BOARD_HEIGHT:
board[y][x] = 0
SpecialType.LINE:
# 直线方块:清除整行
for x in range(BOARD_WIDTH):
board[pos.y][x] = 0
SpecialType.RAINBOW:
# 彩虹方块:清除同色方块
var target_color = board[pos.y][pos.x]
if target_color > 0:
for y in range(BOARD_HEIGHT):
for x in range(BOARD_WIDTH):
if board[y][x] == target_color:
board[y][x] = 0
# 清空特殊方块列表
special_pieces.clear()
func generate_special_piece():
# 在随机位置生成特殊方块
var x = randi() % BOARD_WIDTH
var y = randi() % (BOARD_HEIGHT - 5) + 5 # 在下半部分生成
if board[y][x] != 0: # 只在有方块的位置生成特殊效果
var special_type = randi() % 3 + 1 # 随机选择特殊类型
special_pieces.append({
"position": Vector2(x, y),
"type": special_type
})
# 数据存储功能
func load_player_data():
if FileAccess.file_exists(DATA_FILE_PATH):
var file = FileAccess.open(DATA_FILE_PATH, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result == OK:
player_data = json.data
if player_data.has("tetris"):
var tetris_data = player_data["tetris"]
best_score = tetris_data.get("best_score", 0)
games_played = tetris_data.get("games_played", 0)
total_lines_cleared = tetris_data.get("total_lines_cleared", 0)
func show_start_screen():
# 重置状态
game_started = false
game_over = false
current_piece = null
# 显示开始提示
game_over_label.text = "🎮 俄罗斯方块 🎮\n\n🏆 最高分数: " + str(best_score) + "\n🎯 游戏次数: " + str(games_played) + "\n\n🎮 按Q键开始游戏\n\n🎯 操作说明:\nA/D - 左右移动\nW - 旋转\nS - 快速下落\n空格 - 直接落下"
game_over_label.visible = true
# 停止定时器
drop_timer.stop()
# 清空游戏板
board.clear()
special_pieces.clear()
for y in range(BOARD_HEIGHT):
var row = []
for x in range(BOARD_WIDTH):
row.append(0)
board.append(row)
update_ui()
queue_redraw()
func save_player_data():
if not player_data.has("tetris"):
player_data["tetris"] = {}
player_data["tetris"]["best_score"] = best_score
player_data["tetris"]["games_played"] = games_played
player_data["tetris"]["total_lines_cleared"] = total_lines_cleared
player_data["tetris"]["last_played"] = Time.get_datetime_string_from_system()
var file = FileAccess.open(DATA_FILE_PATH, FileAccess.WRITE)
if file:
var json_string = JSON.stringify(player_data)
file.store_string(json_string)
file.close()
func _draw():
if not game_area:
return
# 绘制背景渐变
var gradient = Gradient.new()
gradient.add_point(0.0, Color(0.05, 0.1, 0.2, 0.95))
gradient.add_point(0.5, Color(0.1, 0.15, 0.25, 0.9))
gradient.add_point(1.0, Color(0.15, 0.2, 0.3, 0.95))
draw_rect(Rect2(Vector2.ZERO, size), gradient.sample(0.5), true)
# 获取游戏区域的位置
var area_pos = game_area.position
var area_size = Vector2(BOARD_WIDTH * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE)
# 绘制游戏区域阴影
var shadow_offset = Vector2(4, 4)
var area_rect = Rect2(area_pos + shadow_offset, area_size)
draw_rect(area_rect, Color(0, 0, 0, 0.4), true)
# 绘制游戏区域背景
area_rect = Rect2(area_pos, area_size)
draw_rect(area_rect, Color(0.08, 0.12, 0.18, 0.9), true)
# 绘制网格线(淡色)
for x in range(BOARD_WIDTH + 1):
var start_pos = Vector2(area_pos.x + x * CELL_SIZE, area_pos.y)
var end_pos = Vector2(area_pos.x + x * CELL_SIZE, area_pos.y + BOARD_HEIGHT * CELL_SIZE)
draw_line(start_pos, end_pos, Color(0.3, 0.3, 0.4, 0.3), 1)
for y in range(BOARD_HEIGHT + 1):
var start_pos = Vector2(area_pos.x, area_pos.y + y * CELL_SIZE)
var end_pos = Vector2(area_pos.x + BOARD_WIDTH * CELL_SIZE, area_pos.y + y * CELL_SIZE)
draw_line(start_pos, end_pos, Color(0.3, 0.3, 0.4, 0.3), 1)
# 绘制游戏板
for y in range(BOARD_HEIGHT):
for x in range(BOARD_WIDTH):
var cell_value = board[y][x]
if cell_value > 0:
var rect = Rect2(
area_pos.x + x * CELL_SIZE,
area_pos.y + y * CELL_SIZE,
CELL_SIZE - 2,
CELL_SIZE - 2
)
var color = PIECE_COLORS[cell_value - 1]
# 检查是否为特殊方块
var is_special = false
var special_type = SpecialType.NORMAL
for special in special_pieces:
if special.position == Vector2(x, y):
is_special = true
special_type = special.type
break
if is_special:
# 绘制特殊方块效果
match special_type:
SpecialType.BOMB:
# 炸弹方块(红色闪烁)
var pulse = sin(Time.get_ticks_msec() * 0.01) * 0.3 + 0.7
draw_rect(rect, Color.RED * pulse, true)
draw_rect(Rect2(rect.position + Vector2(2, 2), rect.size - Vector2(4, 4)), Color.ORANGE, true)
SpecialType.LINE:
# 直线方块(蓝色闪烁)
var pulse = sin(Time.get_ticks_msec() * 0.008) * 0.3 + 0.7
draw_rect(rect, Color.CYAN * pulse, true)
draw_rect(Rect2(rect.position + Vector2(2, 2), rect.size - Vector2(4, 4)), Color.LIGHT_BLUE, true)
SpecialType.RAINBOW:
# 彩虹方块(彩虹色)
var rainbow_time = Time.get_ticks_msec() * 0.003
var rainbow_color = Color.from_hsv(fmod(rainbow_time, 1.0), 1.0, 1.0)
draw_rect(rect, rainbow_color, true)
draw_rect(Rect2(rect.position + Vector2(2, 2), rect.size - Vector2(4, 4)), Color.WHITE, true)
else:
# 普通方块(立体效果)
draw_rect(rect, color, true)
# 高光
var highlight_rect = Rect2(rect.position + Vector2(1, 1), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(highlight_rect, Color(1, 1, 1, 0.3), true)
# 阴影
var shadow_rect = Rect2(rect.position + Vector2(1, rect.size.y * 0.7), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(shadow_rect, Color(0, 0, 0, 0.2), true)
# 绘制当前方块(带透明度预览)
if current_piece:
var shape = get_piece_shape(current_piece.type, current_piece_rotation)
# 绘制投影(显示方块会落在哪里)
var shadow_pos = current_piece_pos
while can_place_piece(shadow_pos + Vector2(0, 1), current_piece_rotation):
shadow_pos.y += 1
for y in range(shape.size()):
for x in range(shape[y].size()):
if shape[y][x] == 1:
var board_x = shadow_pos.x + x
var board_y = shadow_pos.y + y
if board_y >= 0 and board_x >= 0 and board_x < BOARD_WIDTH and board_y < BOARD_HEIGHT:
var rect = Rect2(
area_pos.x + board_x * CELL_SIZE,
area_pos.y + board_y * CELL_SIZE,
CELL_SIZE - 2,
CELL_SIZE - 2
)
var shadow_color = PIECE_COLORS[current_piece.type]
shadow_color.a = 0.3
draw_rect(rect, shadow_color, false, 2)
# 绘制当前方块
for y in range(shape.size()):
for x in range(shape[y].size()):
if shape[y][x] == 1:
var board_x = current_piece_pos.x + x
var board_y = current_piece_pos.y + y
if board_y >= 0:
var rect = Rect2(
area_pos.x + board_x * CELL_SIZE,
area_pos.y + board_y * CELL_SIZE,
CELL_SIZE - 2,
CELL_SIZE - 2
)
var color = PIECE_COLORS[current_piece.type]
# 立体效果
draw_rect(rect, color, true)
# 高光
var highlight_rect = Rect2(rect.position + Vector2(1, 1), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(highlight_rect, Color(1, 1, 1, 0.4), true)
# 绘制下一个方块预览区域背景
var next_area_pos = next_piece_area.position
var next_area_size = next_piece_area.size
draw_rect(Rect2(next_area_pos + Vector2(2, 2), next_area_size), Color(0, 0, 0, 0.3), true)
draw_rect(Rect2(next_area_pos, next_area_size), Color(0.1, 0.15, 0.25, 0.9), true)
# 绘制下一个方块预览
var next_shape = get_piece_shape(next_piece_type, 0)
for y in range(next_shape.size()):
for x in range(next_shape[y].size()):
if next_shape[y][x] == 1:
var rect = Rect2(
next_area_pos.x + 20 + x * 20,
next_area_pos.y + 50 + y * 20,
18,
18
)
var color = PIECE_COLORS[next_piece_type]
# 立体效果
draw_rect(rect, color, true)
# 高光
var highlight_rect = Rect2(rect.position + Vector2(1, 1), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(highlight_rect, Color(1, 1, 1, 0.3), true)
func _on_quit_button_pressed() -> void:
self.hide()
get_parent().remove_child(self)
queue_free()
pass