block.gd 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. class_name Block
  2. extends AnimatableBody2D
  3. ## One arc-shaped segment of a ring around the core. Its [enum Kind] decides how it looks,
  4. ## how many hits it takes and what it does to the ball.
  5. ## Emitted when the segment breaks, whatever broke it.
  6. signal broken(block: Block)
  7. ## Emitted when a TNT segment breaks; the rings turn it into an explosion.
  8. signal detonated(block: Block)
  9. enum Kind { NORMAL, INDESTRUCTIBLE, WATER, ICE, TNT, RUBBER, GHOST, MOSS, MAGNET }
  10. ## What [method strike] did, so the game can pick a sound.
  11. enum Hit { BROKE, DAMAGED, BLOCKED }
  12. const GAP := 4.0 # px between neighbouring tiles
  13. const CORNER := 4.0 # corner rounding radius in px
  14. const BREAK_SECONDS := 0.15 # fade-out time, after a short flash
  15. ## Per kind: name, first level it can appear, hits to break (0 = unbreakable) and flat
  16. ## colour. Water comes early as the first taste; the rest arrive every 50 levels.
  17. ## Normal segments get a random blue-green colour instead.
  18. const KINDS := {
  19. Kind.NORMAL: { name = "Normal", unlock = 0, hp = 1, color = Params.COLOR_BLUE },
  20. Kind.INDESTRUCTIBLE: { name = "Indestructible", unlock = 0, hp = 0, color = Params.COLOR_ORANGE },
  21. Kind.WATER: { name = "Water", unlock = Params.FIRST_SPECIAL_LEVEL, hp = 3, color = Color("#3a7bff") },
  22. Kind.ICE: { name = "Ice", unlock = 50, hp = 2, color = Color("#d6f3ff") },
  23. Kind.TNT: { name = "TNT", unlock = 100, hp = 1, color = Params.COLOR_PINK },
  24. Kind.RUBBER: { name = "Rubber", unlock = 150, hp = 2, color = Color("#ae81ff") },
  25. Kind.GHOST: { name = "Ghost", unlock = 200, hp = 1, color = Color("#c8c3d8") },
  26. Kind.MOSS: { name = "Moss", unlock = 250, hp = 1, color = Params.COLOR_GREEN },
  27. Kind.MAGNET: { name = "Magnet", unlock = 300, hp = 2, color = Color("#9aa4ad") },
  28. }
  29. const WATER_STICK_SECONDS := 0.35
  30. const WATER_SLOWDOWN := 0.85
  31. const ICE_SPEEDUP := 1.08
  32. const RUBBER_SPEEDUP := 1.15
  33. const GHOST_SOLID_SECONDS := 2.0
  34. const GHOST_PHASED_SECONDS := 1.2
  35. const MOSS_REGROW_SECONDS := 8.0 # moss grows back once, this long after breaking
  36. const MAGNET_RADIUS := 150.0
  37. const MAGNET_PULL := 700.0 # px/s² at the segment, fading to 0 at MAGNET_RADIUS
  38. var kind := Kind.NORMAL
  39. var hp := 1
  40. var color := Params.COLOR_BLUE
  41. var _inner := 0.0
  42. var _outer := 0.0
  43. var _begin := 0.0
  44. var _end := 0.0
  45. var _polygon := PackedVector2Array()
  46. var _collision := CollisionPolygon2D.new()
  47. var _cracks := PackedVector2Array()
  48. var _ghost_clock := 0.0
  49. var _regrown := false
  50. ## 0 to 1: how far the tile is washed towards white while breaking.
  51. var _flash := 0.0:
  52. set(value):
  53. _flash = value
  54. queue_redraw()
  55. func _init(inner: float, outer: float, begin: float, end: float, segment_kind: Kind) -> void:
  56. sync_to_physics = false
  57. collision_layer = Params.LAYER_BLOCKS
  58. collision_mask = 0
  59. _inner = inner
  60. _outer = outer
  61. _begin = begin
  62. _end = end
  63. add_child(_collision)
  64. set_kind(segment_kind)
  65. _ghost_clock = randf() * (GHOST_SOLID_SECONDS + GHOST_PHASED_SECONDS)
  66. func set_kind(value: Kind) -> void:
  67. kind = value
  68. hp = KINDS[kind].hp
  69. if kind == Kind.NORMAL:
  70. color = Color8(
  71. int(102 * (randf() / 5.0 + 0.5)),
  72. int(217 * (randf() / 5.0 + 0.5)),
  73. int(239 * (randf() / 5.0 + 0.5)))
  74. else:
  75. color = KINDS[kind].color
  76. _rebuild_shape()
  77. set_physics_process(kind == Kind.GHOST or kind == Kind.MAGNET)
  78. func is_breakable() -> bool:
  79. return hp > 0
  80. ## Centre of the segment in global coordinates.
  81. func center() -> Vector2:
  82. var middle := (_begin + _end) / 2.0
  83. return to_global(Vector2(sin(middle), cos(middle)) * (_inner + _outer) / 2.0)
  84. ## A ball hit the segment. Applies the segment's effect on the ball and takes a hit.
  85. func strike(ball: Ball) -> Hit:
  86. if not is_breakable():
  87. if ball.superball:
  88. ball.superball = false
  89. destroy()
  90. return Hit.BROKE
  91. return Hit.BLOCKED
  92. if ball.fire:
  93. # Fire burns straight through whatever the segment is.
  94. hp = 1
  95. return damage()
  96. match kind:
  97. Kind.ICE:
  98. ball.velocity *= ICE_SPEEDUP
  99. Kind.RUBBER:
  100. ball.velocity *= RUBBER_SPEEDUP
  101. var result := damage()
  102. if kind == Kind.WATER and result == Hit.DAMAGED:
  103. ball.stick_to(self, WATER_STICK_SECONDS, WATER_SLOWDOWN)
  104. return result
  105. ## Takes one hit from anything (ball, laser, explosion). Unbreakable segments ignore it.
  106. func damage() -> Hit:
  107. if not is_breakable() or collision_layer == 0:
  108. return Hit.BLOCKED
  109. hp -= 1
  110. if hp > 0:
  111. _rebuild_shape()
  112. return Hit.DAMAGED
  113. destroy()
  114. return Hit.BROKE
  115. ## Flashes and fades out. Stops colliding right away. Moss comes back once, after a while.
  116. func destroy() -> void:
  117. collision_layer = 0
  118. broken.emit(self)
  119. if kind == Kind.TNT:
  120. detonated.emit(self)
  121. var tween := create_tween()
  122. tween.tween_property(self, "_flash", 0.8, BREAK_SECONDS / 3.0)
  123. tween.tween_property(self, "modulate:a", 0.0, BREAK_SECONDS)
  124. tween.parallel().tween_property(self, "scale", Vector2.ONE * 1.04, BREAK_SECONDS)
  125. if kind == Kind.MOSS and not _regrown:
  126. tween.tween_interval(MOSS_REGROW_SECONDS)
  127. tween.tween_callback(_regrow)
  128. else:
  129. tween.tween_callback(queue_free)
  130. func _regrow() -> void:
  131. _regrown = true
  132. hp = KINDS[kind].hp
  133. _flash = 0.0
  134. scale = Vector2.ONE * 0.6
  135. var tween := create_tween().set_parallel()
  136. tween.tween_property(self, "modulate:a", 1.0, 0.4)
  137. tween.tween_property(self, "scale", Vector2.ONE, 0.4)
  138. tween.chain().tween_callback(set.bind("collision_layer", Params.LAYER_BLOCKS))
  139. func _physics_process(delta: float) -> void:
  140. if kind == Kind.GHOST and hp > 0:
  141. _ghost_clock = fmod(_ghost_clock + delta, GHOST_SOLID_SECONDS + GHOST_PHASED_SECONDS)
  142. var solid := _ghost_clock < GHOST_SOLID_SECONDS
  143. collision_layer = Params.LAYER_BLOCKS if solid else 0
  144. modulate.a = 1.0 if solid else 0.2
  145. elif kind == Kind.MAGNET and collision_layer != 0:
  146. var here := center()
  147. for ball: Ball in get_tree().get_nodes_in_group("balls"):
  148. var offset := here - ball.global_position
  149. var dist := offset.length()
  150. if dist > 0.0 and dist < MAGNET_RADIUS:
  151. ball.velocity += offset / dist * MAGNET_PULL * (1.0 - dist / MAGNET_RADIUS) * delta
  152. ## Recomputes the drawn and collision outline. Water drains from the outside in as it is hit;
  153. ## ice gets cracks; other multi-hit segments darken.
  154. func _rebuild_shape() -> void:
  155. var outer := _outer
  156. var max_hp: int = KINDS[kind].hp
  157. if kind == Kind.WATER and hp < max_hp:
  158. outer = _inner + (_outer - _inner) * float(hp) / max_hp
  159. _polygon = _round_corners(_sector(_inner + GAP / 2.0, outer - GAP / 2.0, _begin, _end, GAP / 2.0))
  160. # Very short segments can leave a sliver too thin to split into convex pieces; skip its
  161. # collision rather than let Godot fail on it.
  162. var valid := not Geometry2D.decompose_polygon_in_convex(_polygon).is_empty()
  163. _collision.set_deferred("polygon", _polygon if valid else PackedVector2Array())
  164. _collision.set_deferred("disabled", not valid)
  165. _cracks = PackedVector2Array()
  166. if kind == Kind.ICE and hp < max_hp:
  167. _cracks = _crack_lines()
  168. queue_redraw()
  169. func _crack_lines() -> PackedVector2Array:
  170. var lines := PackedVector2Array()
  171. var middle := (_begin + _end) / 2.0
  172. var radius := (_inner + _outer) / 2.0
  173. var start := Vector2(sin(middle), cos(middle)) * radius
  174. for i in 3:
  175. var angle := middle + randf_range(-0.6, 0.6) * (_end - _begin) / 2.0
  176. var r := lerpf(_inner + GAP, _outer - GAP, randf())
  177. lines.append(start)
  178. lines.append(Vector2(sin(angle), cos(angle)) * r)
  179. return lines
  180. func _draw() -> void:
  181. if _polygon.size() < 3:
  182. return # segment too short to show at all
  183. var fill := color
  184. if hp > 0 and hp < KINDS[kind].hp and kind != Kind.WATER and kind != Kind.ICE:
  185. fill = fill.darkened(0.25)
  186. fill = fill.lerp(Color.WHITE, _flash)
  187. draw_colored_polygon(_polygon, fill)
  188. # Polygons have no antialiasing of their own; a smooth outline softens the edge.
  189. var outline := _polygon.duplicate()
  190. outline.append(_polygon[0])
  191. draw_polyline(outline, fill, 1.0, true)
  192. if not _cracks.is_empty():
  193. draw_multiline(_cracks, Color(0.2, 0.35, 0.45, 0.9), 1.5, true)
  194. ## Ring segment between two radii. [param gap] trims each side by that many pixels,
  195. ## so the space between neighbours has the same width at every radius. An edge too short
  196. ## to trim collapses to a point, turning the segment into a wedge.
  197. static func _sector(inner: float, outer: float, begin: float, end: float, gap: float) -> PackedVector2Array:
  198. var steps := maxi(2, int((end - begin) * sqrt(outer) / 1.5))
  199. return _trimmed_arc(outer, begin, end, gap, steps) + _trimmed_arc(inner, end, begin, -gap, steps)
  200. static func _trimmed_arc(radius: float, from: float, to: float, gap: float, steps: int) -> PackedVector2Array:
  201. var trim := gap / radius
  202. if (to - from - 2.0 * trim) * signf(to - from) <= 0.0:
  203. var middle := (from + to) / 2.0
  204. return PackedVector2Array([Vector2(sin(middle), cos(middle)) * radius])
  205. return _arc(radius, from + trim, to - trim, steps)
  206. ## Shrinks then regrows the polygon with round joins, which rounds its corners.
  207. static func _round_corners(polygon: PackedVector2Array) -> PackedVector2Array:
  208. var shrunk := Geometry2D.offset_polygon(polygon, -CORNER)
  209. if shrunk.is_empty():
  210. return polygon
  211. var rounded := Geometry2D.offset_polygon(shrunk[0], CORNER, Geometry2D.JOIN_ROUND)
  212. return polygon if rounded.is_empty() else _without_duplicates(rounded[0])
  213. ## offset_polygon can emit repeated points; zero-length edges break convex decomposition.
  214. static func _without_duplicates(polygon: PackedVector2Array) -> PackedVector2Array:
  215. var result := PackedVector2Array()
  216. for point in polygon:
  217. if result.is_empty() or not point.is_equal_approx(result[-1]):
  218. result.append(point)
  219. if result.size() > 1 and result[0].is_equal_approx(result[-1]):
  220. result.remove_at(result.size() - 1)
  221. return result
  222. static func _arc(radius: float, from: float, to: float, steps: int) -> PackedVector2Array:
  223. var points := PackedVector2Array()
  224. for i in steps + 1:
  225. var angle := lerpf(from, to, float(i) / steps)
  226. points.append(Vector2(sin(angle), cos(angle)) * radius)
  227. return points