extends Node2D const TETROMINO_POOL = [ preload("res://tetromino_bar.tscn"), preload("res://tetromino_cube.tscn"), preload("res://tetromino_t.tscn"), preload("res://tetromino_s.tscn"), preload("res://tetromino_z.tscn"), preload("res://tetromino_j.tscn"), preload("res://tetromino_l.tscn"), ] const INPUT_BUFFER_TRHESHOLD := 0.1 var INPUT_BUFFER_MAP: Dictionary[String, float] = { #"ui_up": INPUT_BUFFER_TRHESHOLD, "ui_left": INPUT_BUFFER_TRHESHOLD, "ui_right": INPUT_BUFFER_TRHESHOLD, "ui_down": INPUT_BUFFER_TRHESHOLD, } signal tetromino_reached_bottom signal game_over @export var score_threshold: int var grid: Dictionary[Vector2i, TetrominoSegment] var grid_bounds: Rect2i var current_tetromino: Tetromino var next_tetromino: Tetromino: set = set_next_tetromino var score: int func _ready() -> void: grid_bounds = get_grid_bounds() next_tetromino = TETROMINO_POOL.pick_random().instantiate() _on_tick_timer_timeout() game_over.connect(_on_game_over) func _input(event: InputEvent) -> void: if event.is_action_pressed("ui_up"): rotate_current_tetromino(1.0) func _process(delta: float) -> void: if Input.is_action_pressed("ui_left") and INPUT_BUFFER_MAP["ui_left"] > INPUT_BUFFER_TRHESHOLD: move_current_tetromino(Vector2i.LEFT) INPUT_BUFFER_MAP["ui_left"] = 0.0 if Input.is_action_pressed("ui_right") and INPUT_BUFFER_MAP["ui_right"] > INPUT_BUFFER_TRHESHOLD: move_current_tetromino(Vector2i.RIGHT) INPUT_BUFFER_MAP["ui_right"] = 0.0 if Input.is_action_pressed("ui_down") and INPUT_BUFFER_MAP["ui_down"] > INPUT_BUFFER_TRHESHOLD: move_current_tetromino(Vector2i.DOWN) INPUT_BUFFER_MAP["ui_down"] = 0.0 for key in INPUT_BUFFER_MAP.keys(): INPUT_BUFFER_MAP[key] += delta if OS.is_debug_build(): if current_tetromino: %Debug.text = "P: %s" % current_tetromino.current_grid_position %Debug.text += "\nPS: %s" % " ".join(current_tetromino.get_grid_occupations()) else: %Debug.text = "" func set_next_tetromino(value: Tetromino) -> void: next_tetromino = value %NextTetromino.texture = load("%s.png" % next_tetromino.name) func spawn_tetromino() -> Tetromino: var tetromino: Tetromino = next_tetromino tetromino.init($Grid, $Grid.local_to_map($SpawnPosition.global_position)) return tetromino func set_current_tetromino_to_grid() -> void: var grid_segments := current_tetromino.get_grid_segments() var mapped: Dictionary[int, Variant] for grid_position: Vector2i in grid_segments.keys(): if grid.get(grid_position, null) != null: game_over.emit() break grid.set(grid_position, grid_segments.get(grid_position)) mapped.set(grid_position.y, true) $ProjectionSegments.visible = false clear_rows(mapped.keys()) current_tetromino = null func clear_rows(rows_to_check: Array[int] = []) -> void: if rows_to_check.is_empty(): rows_to_check.assign(range(grid_bounds.size.y)) var filled_rows: Array[int] for y in rows_to_check: var row_is_filled := true for x in range(grid_bounds.size.x): row_is_filled = row_is_filled and grid.get(Vector2i(x, y), null) if not row_is_filled: break if row_is_filled: filled_rows.append(y) # no rows filled? return if filled_rows.size() == 0: return # sort rows filled_rows.sort() $TickTimer.paused = true # animate var tween = create_tween().set_parallel() for y in filled_rows: for x in range(grid_bounds.size.x): var segment: TetrominoSegment = grid.get(Vector2i(x, y)) var previous_modulate := segment.modulate tween.tween_property(segment, "modulate", Color(10, 10, 10, 1), 0.3) tween.tween_property(segment, "modulate", previous_modulate, 0.3).set_delay(0.3) tween.tween_property(segment, "modulate", Color(10, 10, 10, 1), 0.3).set_delay(0.6) tween.tween_property(segment, "modulate", previous_modulate, 0.3).set_delay(0.9) await tween.finished # remove segments for y in filled_rows: for x in range(grid_bounds.size.x): grid.get(Vector2i(x, y)).queue_free() grid.set(Vector2i(x, y), null) # shift rows down for y in filled_rows: for n in range(y - 1, 0, -1): for x in range(grid_bounds.size.x): var segment: TetrominoSegment = grid.get(Vector2i(x, n), null) grid.set(Vector2i(x, n + 1), segment) grid.set(Vector2i(x, n), null) if is_instance_valid(segment): segment.global_position.y += $Grid.tile_set.tile_size.y # increase score and game speed score += int(100 * pow(filled_rows.size(), 2)) %Score.text = str(score) $TickTimer.wait_time = 1.0 - (score / float(score_threshold)) $TickTimer.paused = false func rotate_current_tetromino(direction: float) -> void: if not current_tetromino: return for grid_position in current_tetromino.get_grid_occupations(Vector2.ZERO, direction): if grid.get(grid_position, null) or not grid_bounds.has_point(grid_position): return current_tetromino.do_rotation(direction) update_projection() func move_current_tetromino(direction: Vector2i) -> void: if not current_tetromino: return for grid_position in current_tetromino.get_grid_occupations(direction): if direction == Vector2i.DOWN: if grid.get(grid_position, null) or grid_position.y >= grid_bounds.size.y: set_current_tetromino_to_grid() tetromino_reached_bottom.emit() return elif grid.get(grid_position, null) or not grid_bounds.has_point(grid_position): return current_tetromino.current_grid_position += direction update_projection() func advance_current_tetromino() -> void: move_current_tetromino(Vector2i.DOWN) func update_projection() -> void: var current_segments := current_tetromino.get_grid_occupations() current_segments.sort_custom(func (a: Vector2i, b: Vector2i): return a.y < b.y ) var highest_segment: int = current_segments[0].y var lowest_segment: int = current_segments[3].y var colliding_segment: int = lowest_segment var colliding_row := grid_bounds.size.y var is_colliding := false for y in range(grid_bounds.size.y - highest_segment): for segment in current_segments: if grid.get(Vector2i(segment.x, segment.y + y), null): colliding_row = segment.y + y colliding_segment = segment.y is_colliding = true break elif segment.y + y >= grid_bounds.size.y: is_colliding = true break if is_colliding: break var projection_segments := $ProjectionSegments.get_children() for index in current_segments.size(): var segment := current_segments[index] var difference := colliding_segment - segment.y projection_segments[index].global_position = Vector2( segment.x, colliding_row - 1 - difference ) * Vector2($Grid.tile_set.tile_size) + $Grid.position $ProjectionSegments.visible = true func get_grid_bounds() -> Rect2i: return Rect2i( #Vector2i($Area.global_position - ($Area.shape.size / 2.0)) / $Grid.tile_set.tile_size, Vector2.ZERO, Vector2i($Area.shape.size) / $Grid.tile_set.tile_size ) func _on_tick_timer_timeout() -> void: if current_tetromino: advance_current_tetromino() else: current_tetromino = spawn_tetromino() add_child(current_tetromino) update_projection() next_tetromino = TETROMINO_POOL.pick_random().instantiate() func _on_game_over() -> void: get_tree().quit()