game.gd 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. extends Node2D
  2. ## One level: the core and its rings, the paddle, the balls and the falling power-ups.
  3. const BALL_SCENE := preload("res://scenes/ball.tscn")
  4. const POWER_UP_SCENE := preload("res://scenes/power_up.tscn")
  5. const SUN_BURST_SCENE := preload("res://scenes/sun_burst.tscn")
  6. const SHIELD_HEIGHT := 6.0 # px above the bottom edge
  7. const DEBRIS_PIECES := 10
  8. var level := 0
  9. ## Set once the level is won or lost; stops power-up drops and collision handling.
  10. var over := false
  11. ## When a ball last touched anything (seconds). Long gaps make triple balls more likely.
  12. var last_touch := 0.0
  13. var screen := Vector2.ZERO
  14. ## Balls the shield will still save.
  15. var shield := 0:
  16. set(value):
  17. shield = value
  18. queue_redraw()
  19. var _started := 0.0
  20. var score := 0
  21. ## Segments broken in a row since a ball last touched the paddle.
  22. var combo := 0
  23. ## Screen shake amount, 0 to 1; decays over time.
  24. var _trauma := 0.0
  25. @onready var blocks: Blocks = $Blocks
  26. @onready var paddle: Paddle = $Paddle
  27. @onready var balls: Node2D = $Balls
  28. @onready var power_ups: Node2D = $PowerUps
  29. @onready var alerts: Control = $HUD/Alerts
  30. @onready var end_menu: Control = $HUD/EndMenu
  31. @onready var end_message: Label = $HUD/EndMenu/Message
  32. @onready var end_button: Button = $HUD/EndMenu/Button
  33. @onready var end_stars: Stars = $HUD/EndMenu/Stars
  34. @onready var end_score: Label = $HUD/EndMenu/Score
  35. @onready var score_label: Label = $HUD/Score
  36. @onready var combo_label: Label = $HUD/Combo
  37. @onready var camera: Camera2D = $Camera
  38. func _ready() -> void:
  39. screen = get_viewport_rect().size
  40. level = Progress.current_level
  41. last_touch = _now()
  42. _started = _now()
  43. $HUD/Level.text = "Level %d" % level
  44. paddle.position = Vector2(screen.x / 2.0, screen.y - Params.PADDLE_ALTITUDE - Params.PADDLE_HEIGHT / 2.0)
  45. # The rings sit centred on screen; when there are many, the core moves up so they keep
  46. # their thickness and still leave PADDLE_CLEARANCE above the paddle.
  47. var lowest_ring_edge := paddle.position.y - Params.PADDLE_HEIGHT / 2.0 - Params.PADDLE_CLEARANCE
  48. var natural_radius := Params.CORE_RADIUS + Params.ring_count(level) * Params.RING_SIZE
  49. var core_y := clampf(lowest_ring_edge - natural_radius, screen.y * Params.HIGHEST_CORE, screen.y / 2.0)
  50. blocks.position = Vector2(screen.x / 2.0, core_y)
  51. blocks.build(level, lowest_ring_edge - core_y)
  52. paddle.released.connect(_release_caught_balls)
  53. blocks.segment_broken.connect(_on_segment_broken)
  54. blocks.exploded.connect(func(_at: Vector2) -> void: _shake(0.5))
  55. end_button.pressed.connect(_restart)
  56. _update_score()
  57. Music.play("game")
  58. _drop_power_up()
  59. _drop_triple_balls()
  60. _add_and_throw_ball()
  61. func _on_ball_hit(ball: Ball, other: Node) -> void:
  62. last_touch = _now()
  63. if over:
  64. return
  65. if other is Block:
  66. var block := other as Block
  67. match block.strike(ball):
  68. Block.Hit.BROKE, Block.Hit.DAMAGED:
  69. $Sounds/Pipe.play()
  70. Block.Hit.BLOCKED:
  71. $Sounds/Metal.play()
  72. if ball.bomb:
  73. ball.bomb = false
  74. blocks.explode(ball.global_position, Params.BOMB_RADIUS)
  75. $Sounds/Bomb.play()
  76. elif other is Core:
  77. _win()
  78. elif other is Paddle:
  79. combo = 0
  80. _update_score()
  81. if paddle.catch_time > 0.0:
  82. ball.stick_to(paddle, Params.CATCH_HOLD_SECONDS)
  83. elif ball.velocity.y < 0.0:
  84. ball.aim_from(paddle)
  85. elif other.is_in_group("bottom"):
  86. if shield > 0:
  87. shield -= 1
  88. _alert(Params.MSG_SHIELD_USED)
  89. $Sounds/Metal.play()
  90. else:
  91. _lose_ball(ball)
  92. func _on_power_up_collected(kind: PowerUp.Kind) -> void:
  93. if over:
  94. return
  95. var live := _live_balls()
  96. match kind:
  97. PowerUp.Kind.TRIPLE_BALLS:
  98. _alert(Params.MSG_TRIPLE_BALLS)
  99. for i in 3 - live.size():
  100. _add_and_throw_ball()
  101. PowerUp.Kind.PADDLE:
  102. if randf() < 0.5:
  103. _alert(Params.MSG_DOUBLE_PADDLE)
  104. paddle.double_size()
  105. else:
  106. _alert(Params.MSG_HALF_PADDLE)
  107. paddle.half_size()
  108. PowerUp.Kind.SUPERBALL:
  109. if blocks.has_unbreakable() and not live.is_empty():
  110. _alert(Params.MSG_SUPER_BALL)
  111. live[0].superball = true
  112. PowerUp.Kind.SLOW:
  113. _alert(Params.MSG_SLOW)
  114. for ball: Ball in live:
  115. ball.set_time_scale(Params.SLOW_SCALE, Params.SLOW_SECONDS)
  116. PowerUp.Kind.RUSH:
  117. _alert(Params.MSG_RUSH)
  118. for ball: Ball in live:
  119. ball.set_time_scale(Params.RUSH_SCALE, Params.RUSH_SECONDS)
  120. PowerUp.Kind.CATCH:
  121. _alert(Params.MSG_CATCH)
  122. paddle.set_catch(Params.CATCH_SECONDS)
  123. PowerUp.Kind.SHIELD:
  124. _alert(Params.MSG_SHIELD)
  125. shield = Params.SHIELD_CHARGES
  126. PowerUp.Kind.BIG_BALL:
  127. _alert(Params.MSG_BIG_BALL)
  128. for ball: Ball in live:
  129. ball.set_big(Params.BIG_BALL_SECONDS)
  130. PowerUp.Kind.FIREBALL:
  131. _alert(Params.MSG_FIREBALL)
  132. for ball: Ball in live:
  133. ball.set_fire(Params.FIREBALL_SECONDS)
  134. PowerUp.Kind.LASER:
  135. _alert(Params.MSG_LASER)
  136. paddle.set_laser(Params.LASER_SECONDS)
  137. PowerUp.Kind.FREEZE:
  138. _alert(Params.MSG_FREEZE)
  139. blocks.freeze(Params.FREEZE_SECONDS)
  140. PowerUp.Kind.BOMB:
  141. _alert(Params.MSG_BOMB)
  142. if not live.is_empty():
  143. live[0].bomb = true
  144. PowerUp.Kind.HOMING:
  145. _alert(Params.MSG_HOMING)
  146. for ball: Ball in live:
  147. ball.set_homing(Params.HOMING_SECONDS)
  148. func _release_caught_balls() -> void:
  149. for ball: Ball in _live_balls():
  150. if ball.is_stuck_to(paddle):
  151. ball.release()
  152. ## Score, combo and debris for every segment that breaks, however it broke.
  153. func _on_segment_broken(block: Block) -> void:
  154. if over:
  155. return
  156. combo += 1
  157. var points := Params.POINTS_PER_BREAK * _multiplier()
  158. score += points
  159. _update_score()
  160. var at := block.center()
  161. _debris(at, block.color)
  162. _popup(at, "+%d" % points, Params.COLOR_ORANGE if _multiplier() > 1 else Color.WHITE)
  163. _shake(0.12)
  164. func _multiplier() -> int:
  165. return mini(Params.MAX_COMBO, 1 + combo / Params.BREAKS_PER_COMBO_STEP)
  166. func _update_score() -> void:
  167. score_label.text = str(score)
  168. combo_label.text = "x%d combo" % _multiplier() if _multiplier() > 1 else ""
  169. func _lose_ball(ball: Ball) -> void:
  170. $Sounds/Bomb.play()
  171. _burst(ball.global_position)
  172. _shake(0.6)
  173. ball.queue_free()
  174. if _live_balls().is_empty():
  175. over = true
  176. _clear_play_field()
  177. end_message.text = Params.MSG_OVER
  178. end_message.show()
  179. end_stars.hide()
  180. end_score.text = "Score %d" % score
  181. end_button.text = Params.MSG_RESTART
  182. end_menu.show()
  183. func _win() -> void:
  184. over = true
  185. $Sounds/Win.play()
  186. _shake(0.8)
  187. var seconds := _now() - _started
  188. var par := Params.par_seconds(level)
  189. var stars := 3 if seconds <= par else (2 if seconds <= 2.0 * par else 1)
  190. _clear_play_field()
  191. await blocks.core.explode()
  192. Progress.save_stars(level, stars)
  193. var best := Progress.save_best_score(score)
  194. end_message.text = Params.MSG_LEVEL_DONE % level
  195. end_message.show()
  196. end_stars.show()
  197. end_stars.reveal(stars)
  198. end_score.text = ("New best! " if best else "") + "Score %d" % score
  199. level += 1
  200. Progress.save_level(level)
  201. end_button.text = Params.MSG_NEXT_LEVEL
  202. end_menu.show()
  203. ## Restarts at the current level (the next one after a win).
  204. func _restart() -> void:
  205. Progress.current_level = level
  206. get_tree().reload_current_scene()
  207. func _clear_play_field() -> void:
  208. for node in balls.get_children() + power_ups.get_children():
  209. node.queue_free()
  210. paddle.queue_free()
  211. shield = 0
  212. func _add_and_throw_ball() -> void:
  213. if paddle.is_queued_for_deletion():
  214. return
  215. var ball: Ball = BALL_SCENE.instantiate()
  216. ball.position = paddle.position + Vector2(0, -Params.PADDLE_HEIGHT / 2.0 - Params.BALL_RADIUS - 1.0)
  217. ball.target = blocks.core
  218. ball.max_speed = Params.max_ball_speed(level)
  219. ball.hit.connect(_on_ball_hit)
  220. balls.add_child(ball)
  221. ball.throw(Params.launch_speed(level))
  222. func _live_balls() -> Array:
  223. return balls.get_children().filter(func(ball: Node) -> bool: return not ball.is_queued_for_deletion())
  224. # Power-up drops. Each one rolls its chance, then reschedules itself.
  225. func _drop_power_up() -> void:
  226. if over:
  227. return
  228. if randf() < Params.power_up_chance(level):
  229. var kind := _pick_power_up()
  230. if kind >= 0:
  231. _spawn_power_up(kind as PowerUp.Kind)
  232. _schedule(_drop_power_up)
  233. ## Weighted pick among the power-ups that can drop right now; -1 if none can.
  234. func _pick_power_up() -> int:
  235. var kinds: Array[int] = []
  236. var weights: Array[float] = []
  237. for kind: int in PowerUp.KINDS:
  238. var w := PowerUp.weight(kind as PowerUp.Kind, level)
  239. # A super ball only helps while something unbreakable is left.
  240. if kind == PowerUp.Kind.SUPERBALL and not blocks.has_unbreakable():
  241. w = 0.0
  242. if w > 0.0:
  243. kinds.append(kind)
  244. weights.append(w)
  245. var roll: float = randf() * weights.reduce(func(sum: float, w: float) -> float: return sum + w, 0.0)
  246. for i in kinds.size():
  247. roll -= weights[i]
  248. if roll <= 0.0:
  249. return kinds[i]
  250. return -1
  251. func _drop_triple_balls() -> void:
  252. if over:
  253. return
  254. var idle := _now() - last_touch
  255. if randf() < idle * Params.DROP_TRIPLE_BALL:
  256. last_touch = _now()
  257. if _live_balls().size() < 3:
  258. _spawn_power_up(PowerUp.Kind.TRIPLE_BALLS)
  259. _schedule(_drop_triple_balls)
  260. func _schedule(callback: Callable) -> void:
  261. var interval := Params.POWER_UP_INTERVAL
  262. var delay := interval + randf_range(-1.0, 1.0) * interval / 4.0
  263. get_tree().create_timer(delay).timeout.connect(callback)
  264. func _spawn_power_up(kind: PowerUp.Kind) -> void:
  265. var power_up: PowerUp = POWER_UP_SCENE.instantiate()
  266. power_up.kind = kind
  267. power_up.position = Vector2(randf_range(PowerUp.RADIUS, screen.x - PowerUp.RADIUS), PowerUp.RADIUS + 1.0)
  268. # Collected inside a physics callback, where bodies can't be added or resized
  269. # (triple balls, paddle size, big ball): apply it once physics is done.
  270. power_up.collected.connect(_on_power_up_collected, CONNECT_DEFERRED)
  271. power_ups.add_child(power_up)
  272. func _alert(text: String) -> void:
  273. var label := Label.new()
  274. label.text = text
  275. label.add_theme_font_size_override("font_size", 40)
  276. label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
  277. label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
  278. label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
  279. label.mouse_filter = Control.MOUSE_FILTER_IGNORE
  280. alerts.add_child(label)
  281. label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
  282. var tween := label.create_tween()
  283. tween.tween_property(label, "modulate:a", 0.0, 3.0)
  284. tween.tween_callback(label.queue_free)
  285. ## Chips flying off a broken segment, in its colour.
  286. func _debris(at: Vector2, color: Color) -> void:
  287. var debris := CPUParticles2D.new()
  288. debris.position = at
  289. debris.one_shot = true
  290. debris.amount = DEBRIS_PIECES
  291. debris.lifetime = 0.6
  292. debris.explosiveness = 1.0
  293. debris.spread = 180.0
  294. debris.gravity = Vector2(0, 600)
  295. debris.initial_velocity_min = 60.0
  296. debris.initial_velocity_max = 180.0
  297. debris.scale_amount_min = 3.0
  298. debris.scale_amount_max = 5.0
  299. debris.color = color
  300. var fade := Gradient.new()
  301. fade.set_color(0, Color.WHITE)
  302. fade.set_color(1, Color(1, 1, 1, 0))
  303. debris.color_ramp = fade
  304. debris.finished.connect(debris.queue_free)
  305. add_child(debris)
  306. debris.emitting = true
  307. ## Points floating up from where a segment broke.
  308. func _popup(at: Vector2, text: String, color: Color) -> void:
  309. var label := Label.new()
  310. label.text = text
  311. label.add_theme_font_size_override("font_size", 22)
  312. label.add_theme_color_override("font_color", color)
  313. label.position = at - Vector2(20, 14)
  314. label.z_index = 5
  315. add_child(label)
  316. var tween := label.create_tween().set_parallel()
  317. tween.tween_property(label, "position:y", label.position.y - 40.0, 0.7)
  318. tween.tween_property(label, "modulate:a", 0.0, 0.7).set_delay(0.2)
  319. tween.chain().tween_callback(label.queue_free)
  320. func _shake(amount: float) -> void:
  321. _trauma = minf(1.0, _trauma + amount)
  322. func _process(delta: float) -> void:
  323. _trauma = maxf(0.0, _trauma - Params.SHAKE_DECAY * delta)
  324. var strength := _trauma * _trauma * Params.SHAKE_MAX
  325. camera.offset = Vector2(randf_range(-1, 1), randf_range(-1, 1)) * strength
  326. func _burst(at: Vector2) -> void:
  327. var burst: CPUParticles2D = SUN_BURST_SCENE.instantiate()
  328. burst.position = at
  329. add_child(burst)
  330. func _draw() -> void:
  331. if shield > 0:
  332. var y := screen.y - SHIELD_HEIGHT
  333. draw_line(Vector2(0, y), Vector2(screen.x, y), Color("#f4f4f0"), 4.0, true)
  334. func _now() -> float:
  335. return Time.get_ticks_msec() / 1000.0