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") const SUN_BURST_SCENE := preload("res://scenes/sun_burst.tscn") const SHIELD_HEIGHT := 6.0 # px above the bottom edge 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 ## Balls the shield will still save. var shield := 0: set(value): shield = value queue_redraw() var _started := 0.0 @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() _started = _now() $HUD/Level.text = "Level %d" % level paddle.position = Vector2(screen.x / 2.0, screen.y - Params.PADDLE_ALTITUDE - Params.PADDLE_HEIGHT / 2.0) # The rings sit centred on screen; when there are many, the core moves up so they keep # their thickness and still leave PADDLE_CLEARANCE above the paddle. var lowest_ring_edge := paddle.position.y - Params.PADDLE_HEIGHT / 2.0 - Params.PADDLE_CLEARANCE var natural_radius := Params.CORE_RADIUS + Params.ring_count(level) * Params.RING_SIZE var core_y := clampf(lowest_ring_edge - natural_radius, screen.y * Params.HIGHEST_CORE, screen.y / 2.0) blocks.position = Vector2(screen.x / 2.0, core_y) blocks.build(level, lowest_ring_edge - core_y) paddle.released.connect(_release_caught_balls) end_button.pressed.connect(_restart) _drop_power_up() _drop_triple_balls() _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 match block.strike(ball): Block.Hit.BROKE, Block.Hit.DAMAGED: $Sounds/Pipe.play() Block.Hit.BLOCKED: $Sounds/Metal.play() if ball.bomb: ball.bomb = false blocks.explode(ball.global_position, Params.BOMB_RADIUS) $Sounds/Bomb.play() elif other is Core: _win() elif other is Paddle: if paddle.catch_time > 0.0: ball.stick_to(paddle, Params.CATCH_HOLD_SECONDS) elif ball.velocity.y < 0.0: ball.aim_from(paddle) elif other.is_in_group("bottom"): if shield > 0: shield -= 1 _alert(Params.MSG_SHIELD_USED) $Sounds/Metal.play() else: _lose_ball(ball) func _on_power_up_collected(power_up: PowerUp) -> void: if over: return var live := _live_balls() 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.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: if blocks.has_unbreakable() and not live.is_empty(): _alert(Params.MSG_SUPER_BALL) live[0].superball = true PowerUp.Kind.SLOW: _alert(Params.MSG_SLOW) for ball: Ball in live: ball.set_time_scale(Params.SLOW_SCALE, Params.SLOW_SECONDS) PowerUp.Kind.RUSH: _alert(Params.MSG_RUSH) for ball: Ball in live: ball.set_time_scale(Params.RUSH_SCALE, Params.RUSH_SECONDS) PowerUp.Kind.CATCH: _alert(Params.MSG_CATCH) paddle.set_catch(Params.CATCH_SECONDS) PowerUp.Kind.SHIELD: _alert(Params.MSG_SHIELD) shield = Params.SHIELD_CHARGES PowerUp.Kind.BIG_BALL: _alert(Params.MSG_BIG_BALL) for ball: Ball in live: ball.set_big(Params.BIG_BALL_SECONDS) PowerUp.Kind.FIREBALL: _alert(Params.MSG_FIREBALL) for ball: Ball in live: ball.set_fire(Params.FIREBALL_SECONDS) PowerUp.Kind.LASER: _alert(Params.MSG_LASER) paddle.set_laser(Params.LASER_SECONDS) PowerUp.Kind.FREEZE: _alert(Params.MSG_FREEZE) blocks.freeze(Params.FREEZE_SECONDS) PowerUp.Kind.BOMB: _alert(Params.MSG_BOMB) if not live.is_empty(): live[0].bomb = true PowerUp.Kind.HOMING: _alert(Params.MSG_HOMING) for ball: Ball in live: ball.set_homing(Params.HOMING_SECONDS) func _release_caught_balls() -> void: for ball: Ball in _live_balls(): if ball.is_stuck_to(paddle): ball.release() 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() shield = 0 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.target = blocks.core ball.max_speed = Params.max_ball_speed(level) ball.hit.connect(_on_ball_hit) balls.add_child(ball) ball.throw(Params.launch_speed(level)) 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_power_up() -> void: if over: return if randf() < Params.power_up_chance(level): var kind := _pick_power_up() if kind >= 0: _spawn_power_up(kind as PowerUp.Kind) _schedule(_drop_power_up) ## Weighted pick among the power-ups that can drop right now; -1 if none can. func _pick_power_up() -> int: var kinds: Array[int] = [] var weights: Array[float] = [] for kind: int in PowerUp.KINDS: var w := PowerUp.weight(kind as PowerUp.Kind, level) if kind == PowerUp.Kind.SAVE and _now() - _started < Params.SAVE_DELAY: w = 0.0 # A super ball only helps while something unbreakable is left. if kind == PowerUp.Kind.SUPERBALL and not blocks.has_unbreakable(): w = 0.0 if w > 0.0: kinds.append(kind) weights.append(w) var roll: float = randf() * weights.reduce(func(sum: float, w: float) -> float: return sum + w, 0.0) for i in kinds.size(): roll -= weights[i] if roll <= 0.0: return kinds[i] return -1 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 _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.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART 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) func _burst(at: Vector2) -> void: var burst: CPUParticles2D = SUN_BURST_SCENE.instantiate() burst.position = at add_child(burst) func _draw() -> void: if shield > 0: var y := screen.y - SHIELD_HEIGHT draw_line(Vector2(0, y), Vector2(screen.x, y), Color("#f4f4f0"), 4.0, true) func _now() -> float: return Time.get_ticks_msec() / 1000.0