extends Node2D ## One level: the core and its rings, the paddle, the balls and the falling power-ups. const BALL_SCENE := preload("res://scenes/ball.tscn") const POWER_UP_SCENE := preload("res://scenes/power_up.tscn") var level := 0 ## Set once the level is won or lost; stops power-up drops and collision handling. var over := false ## When a ball last touched anything (seconds). Long gaps make triple balls more likely. var last_touch := 0.0 var screen := Vector2.ZERO @onready var blocks: Blocks = $Blocks @onready var paddle: Paddle = $Paddle @onready var balls: Node2D = $Balls @onready var power_ups: Node2D = $PowerUps @onready var alerts: Control = $HUD/Alerts @onready var end_menu: Control = $HUD/EndMenu @onready var end_message: Label = $HUD/EndMenu/Message @onready var end_button: Button = $HUD/EndMenu/Button func _ready() -> void: screen = get_viewport_rect().size level = Progress.current_level last_touch = _now() $HUD/Level.text = "Level %d" % level blocks.position = screen / 2.0 blocks.build(level) paddle.position = Vector2(screen.x / 2.0, screen.y - Params.PADDLE_ALTITUDE - Params.PADDLE_HEIGHT / 2.0) end_button.pressed.connect(_restart) get_tree().create_timer(10.0).timeout.connect(_drop_level_save) _drop_triple_balls() _drop_paddle() _drop_superball() _add_and_throw_ball() func _on_ball_hit(ball: Ball, other: Node) -> void: last_touch = _now() if over: return if other is Block: var block := other as Block if not block.indestructible: $Sounds/Pipe.play() block.destroy() else: $Sounds/Metal.play() if ball.superball: block.destroy() ball.superball = false elif other is Core: _win() elif other.is_in_group("bottom"): _lose_ball(ball) func _on_power_up_collected(power_up: PowerUp) -> void: if over: return match power_up.kind: PowerUp.Kind.SAVE: Progress.save_level(level) _alert(Params.MSG_LEVEL_SAVED) PowerUp.Kind.TRIPLE_BALLS: _alert(Params.MSG_TRIPLE_BALLS) for i in 3 - _live_balls().size(): _add_and_throw_ball() PowerUp.Kind.PADDLE: if randf() < 0.5: _alert(Params.MSG_DOUBLE_PADDLE) paddle.double_size() else: _alert(Params.MSG_HALF_PADDLE) paddle.half_size() PowerUp.Kind.SUPERBALL: _alert(Params.MSG_SUPER_BALL) var live := _live_balls() if not live.is_empty(): live[0].superball = true func _lose_ball(ball: Ball) -> void: $Sounds/Bomb.play() _burst(ball.global_position) ball.queue_free() if _live_balls().is_empty(): over = true _clear_play_field() end_message.text = Params.MSG_OVER end_message.show() end_button.text = Params.MSG_RESTART end_menu.show() func _win() -> void: over = true $Sounds/Win.play() _clear_play_field() await blocks.core.explode() level += 1 end_message.hide() end_button.text = Params.MSG_NEXT_LEVEL end_menu.show() ## Restarts at the current level (the next one after a win). func _restart() -> void: Progress.current_level = level get_tree().reload_current_scene() func _clear_play_field() -> void: for node in balls.get_children() + power_ups.get_children(): node.queue_free() paddle.queue_free() func _add_and_throw_ball() -> void: if paddle.is_queued_for_deletion(): return var ball: Ball = BALL_SCENE.instantiate() ball.position = paddle.position + Vector2(0, -Params.PADDLE_HEIGHT / 2.0 - Params.BALL_RADIUS - 1.0) ball.hit.connect(_on_ball_hit) balls.add_child(ball) ball.throw() func _live_balls() -> Array: return balls.get_children().filter(func(ball: Node) -> bool: return not ball.is_queued_for_deletion()) # Power-up drops. Each one rolls its chance, then reschedules itself. func _drop_level_save() -> void: if over: return if randf() < Params.DROP_LEVEL_SAVE: _spawn_power_up(PowerUp.Kind.SAVE) _schedule(_drop_level_save) func _drop_triple_balls() -> void: if over: return var idle := _now() - last_touch if randf() < idle * Params.DROP_TRIPLE_BALL: last_touch = _now() if _live_balls().size() < 3: _spawn_power_up(PowerUp.Kind.TRIPLE_BALLS) _schedule(_drop_triple_balls) func _drop_paddle() -> void: if over: return if randf() < Params.DROP_PADDLE_BALL: _spawn_power_up(PowerUp.Kind.PADDLE) _schedule(_drop_paddle) func _drop_superball() -> void: if over: return if randf() < Params.DROP_SUPER_BALL: _spawn_power_up(PowerUp.Kind.SUPERBALL) _schedule(_drop_superball) func _schedule(callback: Callable) -> void: var interval := Params.POWER_UP_INTERVAL var delay := interval + randf_range(-1.0, 1.0) * interval / 4.0 get_tree().create_timer(delay).timeout.connect(callback) func _spawn_power_up(kind: PowerUp.Kind) -> void: var power_up: PowerUp = POWER_UP_SCENE.instantiate() power_up.kind = kind power_up.position = Vector2(randf_range(PowerUp.RADIUS, screen.x - PowerUp.RADIUS), PowerUp.RADIUS + 1.0) power_up.collected.connect(_on_power_up_collected) power_ups.add_child(power_up) func _alert(text: String) -> void: var label := Label.new() label.text = text label.add_theme_font_size_override("font_size", 40) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE alerts.add_child(label) label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var tween := label.create_tween() tween.tween_property(label, "modulate:a", 0.0, 3.0) tween.tween_callback(label.queue_free) ## A short orange flash where a ball fell out. func _burst(at: Vector2) -> void: var particles := CPUParticles2D.new() particles.position = at particles.one_shot = true particles.amount = 80 particles.lifetime = 1.0 particles.explosiveness = 0.9 particles.spread = 180.0 particles.gravity = Vector2.ZERO particles.initial_velocity_min = 40.0 particles.initial_velocity_max = 160.0 particles.scale_amount_min = 3.0 particles.scale_amount_max = 6.0 var ramp := Gradient.new() ramp.set_color(0, Color(1.0, 0.85, 0.3)) ramp.set_color(1, Color(1.0, 0.3, 0.0, 0.0)) particles.color_ramp = ramp particles.finished.connect(particles.queue_free) add_child(particles) particles.emitting = true func _now() -> float: return Time.get_ticks_msec() / 1000.0