| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- 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)
|