1
0

ball.gd 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. class_name Ball
  2. extends CharacterBody2D
  3. ## A ball that bounces off everything, speeds up slightly on each bounce and falls under
  4. ## light gravity. Power-ups change it for a while (see the timed effects below).
  5. ## Emitted for every contact, so the game can react to what the ball touched.
  6. signal hit(ball: Ball, other: Node)
  7. const BIG_SCALE := 1.6
  8. const HOMING_TURN := 1.5 # radians per second towards the target
  9. ## A superball destroys one indestructible block, then turns back into a normal ball.
  10. var superball := false:
  11. set(value):
  12. superball = value
  13. queue_redraw()
  14. ## The next block hit explodes.
  15. var bomb := false:
  16. set(value):
  17. bomb = value
  18. queue_redraw()
  19. ## Fire burns through breakable blocks without bouncing.
  20. var fire: bool:
  21. get:
  22. return _fire_time > 0.0
  23. ## What homing steers towards (the core).
  24. var target: Node2D
  25. var radius := Params.BALL_RADIUS
  26. ## Bounces and speed-ups stop here; the game raises it with the level.
  27. var max_speed := Params.max_ball_speed(0)
  28. var _fire_time := 0.0
  29. var _big_time := 0.0
  30. var _homing_time := 0.0
  31. var _time_scale := 1.0
  32. var _time_scale_left := 0.0
  33. var _stuck_to: Node2D
  34. var _stuck_offset := Vector2.ZERO
  35. var _stuck_time := 0.0
  36. var _stuck_slowdown := 1.0
  37. var _shape := CircleShape2D.new()
  38. func _ready() -> void:
  39. add_to_group("balls")
  40. _shape.radius = radius
  41. $CollisionShape2D.shape = _shape
  42. func throw(speed := Params.INITIAL_SPEED) -> void:
  43. var angle := deg_to_rad(randf_range(-Params.THROW_ANGLE, Params.THROW_ANGLE))
  44. velocity = Vector2(sin(angle), -cos(angle)) * speed
  45. func set_fire(seconds: float) -> void:
  46. _fire_time = maxf(_fire_time, seconds)
  47. queue_redraw()
  48. func set_big(seconds: float) -> void:
  49. _big_time = maxf(_big_time, seconds)
  50. _set_radius(Params.BALL_RADIUS * BIG_SCALE)
  51. func set_homing(seconds: float) -> void:
  52. _homing_time = maxf(_homing_time, seconds)
  53. queue_redraw()
  54. ## Runs the ball faster (> 1) or slower (< 1) for a while; the path stays the same.
  55. func set_time_scale(value: float, seconds: float) -> void:
  56. _time_scale = value
  57. _time_scale_left = seconds
  58. queue_redraw()
  59. ## Holds the ball on [param node] (a paddle or a water segment), moving with it, for
  60. ## [param seconds]; then it leaves with its speed times [param slowdown].
  61. func stick_to(node: Node2D, seconds: float, slowdown := 1.0) -> void:
  62. _stuck_to = node
  63. _stuck_offset = node.to_local(global_position)
  64. _stuck_time = seconds
  65. _stuck_slowdown = slowdown
  66. func is_stuck_to(node: Node2D) -> bool:
  67. return _stuck_to == node
  68. ## Sends the ball up off the paddle at an angle set by where it hit: straight up from the
  69. ## centre, up to PADDLE_MAX_ANGLE from vertical at the tips. Keeps its speed.
  70. func aim_from(paddle: Paddle, speed := velocity.length()) -> void:
  71. var side := clampf((global_position.x - paddle.global_position.x) / paddle.half_width(), -1.0, 1.0)
  72. var angle := deg_to_rad(side * Params.PADDLE_MAX_ANGLE)
  73. velocity = Vector2(sin(angle), -cos(angle)) * speed
  74. ## Lets go of whatever holds it. Off the paddle, the angle depends on where it sat.
  75. func release() -> void:
  76. if _stuck_to == null:
  77. return
  78. if _stuck_to is Paddle and is_instance_valid(_stuck_to):
  79. aim_from(_stuck_to as Paddle, maxf(velocity.length(), Params.INITIAL_SPEED))
  80. velocity *= _stuck_slowdown
  81. _stuck_to = null
  82. func _physics_process(delta: float) -> void:
  83. _tick_effects(delta)
  84. if _stuck_to != null:
  85. if not is_instance_valid(_stuck_to) or _stuck_to.is_queued_for_deletion():
  86. _stuck_to = null
  87. else:
  88. global_position = _stuck_to.to_global(_stuck_offset)
  89. _stuck_time -= delta
  90. if _stuck_time <= 0.0:
  91. release()
  92. return
  93. var dt := delta * _time_scale
  94. velocity.y += Params.BALL_GRAVITY * dt
  95. if _homing_time > 0.0 and is_instance_valid(target):
  96. var turn := velocity.angle_to(target.global_position - global_position)
  97. velocity = velocity.rotated(clampf(turn, -HOMING_TURN * dt, HOMING_TURN * dt))
  98. var motion := velocity * dt
  99. for i in 4:
  100. var collision := move_and_collide(motion)
  101. if collision == null:
  102. break
  103. var other := collision.get_collider() as Node
  104. var normal := collision.get_normal()
  105. # Fire burns through breakable blocks: no bounce, keep going.
  106. var burn := fire and other is Block and (other as Block).is_breakable()
  107. if not burn:
  108. var bounce: float = other.get_meta("bounce", 1.0)
  109. velocity = velocity.bounce(normal) * Params.BALL_BOUNCE * bounce
  110. motion = collision.get_remainder().bounce(normal)
  111. else:
  112. motion = collision.get_remainder()
  113. hit.emit(self, other)
  114. velocity = velocity.limit_length(max_speed)
  115. if is_queued_for_deletion() or _stuck_to != null:
  116. return
  117. _kick_power_ups()
  118. func _tick_effects(delta: float) -> void:
  119. var was := [_fire_time > 0.0, _big_time > 0.0, _homing_time > 0.0, _time_scale_left > 0.0]
  120. _fire_time = maxf(_fire_time - delta, 0.0)
  121. _big_time = maxf(_big_time - delta, 0.0)
  122. _homing_time = maxf(_homing_time - delta, 0.0)
  123. _time_scale_left = maxf(_time_scale_left - delta, 0.0)
  124. if was[1] and _big_time == 0.0:
  125. _set_radius(Params.BALL_RADIUS)
  126. if was[3] and _time_scale_left == 0.0:
  127. _time_scale = 1.0
  128. if was != [_fire_time > 0.0, _big_time > 0.0, _homing_time > 0.0, _time_scale_left > 0.0]:
  129. queue_redraw()
  130. func _set_radius(value: float) -> void:
  131. radius = value
  132. _shape.radius = value
  133. queue_redraw()
  134. ## Power-ups are 1/1000 of a ball's mass: they get knocked away and the ball doesn't notice.
  135. func _kick_power_ups() -> void:
  136. for power_up: PowerUp in get_tree().get_nodes_in_group("power_ups"):
  137. var offset := power_up.global_position - global_position
  138. var dist := offset.length()
  139. if dist == 0.0 or dist > radius + PowerUp.RADIUS:
  140. continue
  141. var normal := offset / dist
  142. var approach := (power_up.linear_velocity - velocity).dot(normal)
  143. if approach < 0.0:
  144. power_up.linear_velocity -= 2.0 * approach * normal
  145. hit.emit(self, power_up)
  146. func _draw() -> void:
  147. var fill := Params.COLOR_GREY
  148. if superball:
  149. fill = Params.COLOR_ORANGE
  150. elif fire:
  151. fill = Params.COLOR_FIRE
  152. elif bomb:
  153. fill = Params.COLOR_BOMB
  154. draw_circle(Vector2.ZERO, radius, fill, true, -1.0, true)
  155. if bomb:
  156. draw_circle(Vector2.ZERO, radius - 1.5, Params.COLOR_ORANGE, false, 3.0, true)
  157. # Thin rings show timed effects: homing in red, slow-mo in blue, rush in pink.
  158. var ring := radius + 4.0
  159. if _homing_time > 0.0:
  160. draw_circle(Vector2.ZERO, ring, Params.COLOR_RED, false, 2.0, true)
  161. ring += 4.0
  162. if _time_scale_left > 0.0:
  163. draw_circle(Vector2.ZERO, ring, Params.COLOR_BLUE if _time_scale < 1.0 else Params.COLOR_PINK, false, 2.0, true)