diff options
| author | Daniel Weipert <git@mail.dweipert.de> | 2026-03-21 11:23:59 +0100 |
|---|---|---|
| committer | Daniel Weipert <git@mail.dweipert.de> | 2026-03-21 11:23:59 +0100 |
| commit | 271f1db35e1654d0e25e2025c86d46ba401d288e (patch) | |
| tree | 3ea40d061254d2a7745b941ef309fc71fc4a06d7 /main.gd | |
Diffstat (limited to 'main.gd')
| -rw-r--r-- | main.gd | 260 |
1 files changed, 260 insertions, 0 deletions
@@ -0,0 +1,260 @@ +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() |
