class_name Ball extends CharacterBody2D ## A ball that bounces off everything, speeds up slightly on each bounce and falls under ## light gravity. Power-ups change it for a while (see the timed effects below). ## Emitted for every contact, so the game can react to what the ball touched. signal hit(ball: Ball, other: Node) const BIG_SCALE := 1.6 const HOMING_TURN := 1.5 # radians per second towards the target ## A superball destroys one indestructible block, then turns back into a normal ball. var superball := false: set(value): superball = value queue_redraw() ## The next block hit explodes. var bomb := false: set(value): bomb = value queue_redraw() ## Fire burns through breakable blocks without bouncing. var fire: bool: get: return _fire_time > 0.0 ## What homing steers towards (the core). var target: Node2D var radius := Params.BALL_RADIUS ## Bounces and speed-ups stop here; the game raises it with the level. var max_speed := Params.max_ball_speed(0) var _fire_time := 0.0 var _big_time := 0.0 var _homing_time := 0.0 var _time_scale := 1.0 var _time_scale_left := 0.0 var _stuck_to: Node2D var _stuck_offset := Vector2.ZERO var _stuck_time := 0.0 var _stuck_slowdown := 1.0 var _shape := CircleShape2D.new() func _ready() -> void: add_to_group("balls") _shape.radius = radius $CollisionShape2D.shape = _shape func throw(speed := Params.INITIAL_SPEED) -> void: var angle := deg_to_rad(randf_range(-Params.THROW_ANGLE, Params.THROW_ANGLE)) velocity = Vector2(sin(angle), -cos(angle)) * speed func set_fire(seconds: float) -> void: _fire_time = maxf(_fire_time, seconds) queue_redraw() func set_big(seconds: float) -> void: _big_time = maxf(_big_time, seconds) _set_radius(Params.BALL_RADIUS * BIG_SCALE) func set_homing(seconds: float) -> void: _homing_time = maxf(_homing_time, seconds) queue_redraw() ## Runs the ball faster (> 1) or slower (< 1) for a while; the path stays the same. func set_time_scale(value: float, seconds: float) -> void: _time_scale = value _time_scale_left = seconds queue_redraw() ## Holds the ball on [param node] (a paddle or a water segment), moving with it, for ## [param seconds]; then it leaves with its speed times [param slowdown]. func stick_to(node: Node2D, seconds: float, slowdown := 1.0) -> void: _stuck_to = node _stuck_offset = node.to_local(global_position) _stuck_time = seconds _stuck_slowdown = slowdown func is_stuck_to(node: Node2D) -> bool: return _stuck_to == node ## Sends the ball up off the paddle at an angle set by where it hit: straight up from the ## centre, up to PADDLE_MAX_ANGLE from vertical at the tips. Keeps its speed. func aim_from(paddle: Paddle, speed := velocity.length()) -> void: var side := clampf((global_position.x - paddle.global_position.x) / paddle.half_width(), -1.0, 1.0) var angle := deg_to_rad(side * Params.PADDLE_MAX_ANGLE) velocity = Vector2(sin(angle), -cos(angle)) * speed ## Lets go of whatever holds it. Off the paddle, the angle depends on where it sat. func release() -> void: if _stuck_to == null: return if _stuck_to is Paddle and is_instance_valid(_stuck_to): aim_from(_stuck_to as Paddle, maxf(velocity.length(), Params.INITIAL_SPEED)) velocity *= _stuck_slowdown _stuck_to = null func _physics_process(delta: float) -> void: _tick_effects(delta) if _stuck_to != null: if not is_instance_valid(_stuck_to) or _stuck_to.is_queued_for_deletion(): _stuck_to = null else: global_position = _stuck_to.to_global(_stuck_offset) _stuck_time -= delta if _stuck_time <= 0.0: release() return var dt := delta * _time_scale velocity.y += Params.BALL_GRAVITY * dt if _homing_time > 0.0 and is_instance_valid(target): var turn := velocity.angle_to(target.global_position - global_position) velocity = velocity.rotated(clampf(turn, -HOMING_TURN * dt, HOMING_TURN * dt)) var motion := velocity * dt for i in 4: var collision := move_and_collide(motion) if collision == null: break var other := collision.get_collider() as Node var normal := collision.get_normal() # Fire burns through breakable blocks: no bounce, keep going. var burn := fire and other is Block and (other as Block).is_breakable() if not burn: var bounce: float = other.get_meta("bounce", 1.0) velocity = velocity.bounce(normal) * Params.BALL_BOUNCE * bounce motion = collision.get_remainder().bounce(normal) else: motion = collision.get_remainder() hit.emit(self, other) velocity = velocity.limit_length(max_speed) if is_queued_for_deletion() or _stuck_to != null: return _kick_power_ups() func _tick_effects(delta: float) -> void: var was := [_fire_time > 0.0, _big_time > 0.0, _homing_time > 0.0, _time_scale_left > 0.0] _fire_time = maxf(_fire_time - delta, 0.0) _big_time = maxf(_big_time - delta, 0.0) _homing_time = maxf(_homing_time - delta, 0.0) _time_scale_left = maxf(_time_scale_left - delta, 0.0) if was[1] and _big_time == 0.0: _set_radius(Params.BALL_RADIUS) if was[3] and _time_scale_left == 0.0: _time_scale = 1.0 if was != [_fire_time > 0.0, _big_time > 0.0, _homing_time > 0.0, _time_scale_left > 0.0]: queue_redraw() func _set_radius(value: float) -> void: radius = value _shape.radius = value queue_redraw() ## Power-ups are 1/1000 of a ball's mass: they get knocked away and the ball doesn't notice. func _kick_power_ups() -> void: for power_up: PowerUp in get_tree().get_nodes_in_group("power_ups"): var offset := power_up.global_position - global_position var dist := offset.length() if dist == 0.0 or dist > radius + PowerUp.RADIUS: continue var normal := offset / dist var approach := (power_up.linear_velocity - velocity).dot(normal) if approach < 0.0: power_up.linear_velocity -= 2.0 * approach * normal hit.emit(self, power_up) func _draw() -> void: var fill := Params.COLOR_GREY if superball: fill = Params.COLOR_ORANGE elif fire: fill = Params.COLOR_FIRE elif bomb: fill = Params.COLOR_BOMB draw_circle(Vector2.ZERO, radius, fill, true, -1.0, true) if bomb: draw_circle(Vector2.ZERO, radius - 1.5, Params.COLOR_ORANGE, false, 3.0, true) # Thin rings show timed effects: homing in red, slow-mo in blue, rush in pink. var ring := radius + 4.0 if _homing_time > 0.0: draw_circle(Vector2.ZERO, ring, Params.COLOR_RED, false, 2.0, true) ring += 4.0 if _time_scale_left > 0.0: draw_circle(Vector2.ZERO, ring, Params.COLOR_BLUE if _time_scale < 1.0 else Params.COLOR_PINK, false, 2.0, true)