summaryrefslogtreecommitdiff
path: root/main.gd
diff options
context:
space:
mode:
authorDaniel Weipert <git@mail.dweipert.de>2026-03-21 11:23:59 +0100
committerDaniel Weipert <git@mail.dweipert.de>2026-03-21 11:23:59 +0100
commit271f1db35e1654d0e25e2025c86d46ba401d288e (patch)
tree3ea40d061254d2a7745b941ef309fc71fc4a06d7 /main.gd
initial commitHEADmain
Diffstat (limited to 'main.gd')
-rw-r--r--main.gd260
1 files changed, 260 insertions, 0 deletions
diff --git a/main.gd b/main.gd
new file mode 100644
index 0000000..aef207c
--- /dev/null
+++ b/main.gd
@@ -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()