1
0

game.gd 12 KB

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