|
|
@@ -1,14 +1,56 @@
|
|
|
class_name Block
|
|
|
extends AnimatableBody2D
|
|
|
-## One arc-shaped segment of a ring around the core.
|
|
|
+## One arc-shaped segment of a ring around the core. Its [enum Kind] decides how it looks,
|
|
|
+## how many hits it takes and what it does to the ball.
|
|
|
+
|
|
|
+## Emitted when a TNT segment breaks; the rings turn it into an explosion.
|
|
|
+signal detonated(block: Block)
|
|
|
+
|
|
|
+enum Kind { NORMAL, INDESTRUCTIBLE, WATER, ICE, TNT, RUBBER, GHOST, MOSS, MAGNET }
|
|
|
+
|
|
|
+## What [method strike] did, so the game can pick a sound.
|
|
|
+enum Hit { BROKE, DAMAGED, BLOCKED }
|
|
|
|
|
|
const GAP := 4.0 # px between neighbouring tiles
|
|
|
const CORNER := 4.0 # corner rounding radius in px
|
|
|
const BREAK_SECONDS := 0.15 # fade-out time, after a short flash
|
|
|
|
|
|
-var indestructible := false
|
|
|
+## Per kind: first level it can appear, hits to break (0 = unbreakable) and flat colour.
|
|
|
+## Normal segments get a random blue-green colour instead.
|
|
|
+const KINDS := {
|
|
|
+ Kind.NORMAL: { unlock = 0, hp = 1, color = Params.COLOR_BLUE },
|
|
|
+ Kind.INDESTRUCTIBLE: { unlock = 0, hp = 0, color = Params.COLOR_ORANGE },
|
|
|
+ Kind.WATER: { unlock = 50, hp = 3, color = Color("#3a7bff") },
|
|
|
+ Kind.ICE: { unlock = 100, hp = 2, color = Color("#d6f3ff") },
|
|
|
+ Kind.TNT: { unlock = 150, hp = 1, color = Params.COLOR_PINK },
|
|
|
+ Kind.RUBBER: { unlock = 200, hp = 2, color = Color("#ae81ff") },
|
|
|
+ Kind.GHOST: { unlock = 250, hp = 1, color = Color("#c8c3d8") },
|
|
|
+ Kind.MOSS: { unlock = 300, hp = 1, color = Params.COLOR_GREEN },
|
|
|
+ Kind.MAGNET: { unlock = 350, hp = 2, color = Color("#9aa4ad") },
|
|
|
+}
|
|
|
+
|
|
|
+const WATER_STICK_SECONDS := 0.35
|
|
|
+const WATER_SLOWDOWN := 0.85
|
|
|
+const ICE_SPEEDUP := 1.08
|
|
|
+const RUBBER_SPEEDUP := 1.15
|
|
|
+const GHOST_SOLID_SECONDS := 2.0
|
|
|
+const GHOST_PHASED_SECONDS := 1.2
|
|
|
+const MOSS_REGROW_SECONDS := 8.0 # moss grows back once, this long after breaking
|
|
|
+const MAGNET_RADIUS := 150.0
|
|
|
+const MAGNET_PULL := 700.0 # px/s² at the segment, fading to 0 at MAGNET_RADIUS
|
|
|
+
|
|
|
+var kind := Kind.NORMAL
|
|
|
+var hp := 1
|
|
|
var color := Params.COLOR_BLUE
|
|
|
+var _inner := 0.0
|
|
|
+var _outer := 0.0
|
|
|
+var _begin := 0.0
|
|
|
+var _end := 0.0
|
|
|
var _polygon := PackedVector2Array()
|
|
|
+var _collision := CollisionPolygon2D.new()
|
|
|
+var _cracks := PackedVector2Array()
|
|
|
+var _ghost_clock := 0.0
|
|
|
+var _regrown := false
|
|
|
## 0 to 1: how far the tile is washed towards white while breaking.
|
|
|
var _flash := 0.0:
|
|
|
set(value):
|
|
|
@@ -16,49 +58,166 @@ var _flash := 0.0:
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
-func _init(inner: float, outer: float, begin: float, end: float, is_indestructible: bool) -> void:
|
|
|
+func _init(inner: float, outer: float, begin: float, end: float, segment_kind: Kind) -> void:
|
|
|
sync_to_physics = false
|
|
|
collision_layer = Params.LAYER_BLOCKS
|
|
|
collision_mask = 0
|
|
|
- indestructible = is_indestructible
|
|
|
+ _inner = inner
|
|
|
+ _outer = outer
|
|
|
+ _begin = begin
|
|
|
+ _end = end
|
|
|
+ add_child(_collision)
|
|
|
+ set_kind(segment_kind)
|
|
|
+ _ghost_clock = randf() * (GHOST_SOLID_SECONDS + GHOST_PHASED_SECONDS)
|
|
|
|
|
|
- # Collision uses the drawn outline, so the ball bounces exactly where it touches.
|
|
|
- _polygon = _round_corners(_sector(inner + GAP / 2.0, outer - GAP / 2.0, begin, end, GAP / 2.0))
|
|
|
- # Very short segments can leave a sliver too thin to split into convex pieces; skip its
|
|
|
- # collision rather than let Godot fail on it.
|
|
|
- if not Geometry2D.decompose_polygon_in_convex(_polygon).is_empty():
|
|
|
- var collision := CollisionPolygon2D.new()
|
|
|
- collision.polygon = _polygon
|
|
|
- add_child(collision)
|
|
|
|
|
|
- if indestructible:
|
|
|
- color = Params.COLOR_ORANGE
|
|
|
- else:
|
|
|
+func set_kind(value: Kind) -> void:
|
|
|
+ kind = value
|
|
|
+ hp = KINDS[kind].hp
|
|
|
+ if kind == Kind.NORMAL:
|
|
|
color = Color8(
|
|
|
int(102 * (randf() / 5.0 + 0.5)),
|
|
|
int(217 * (randf() / 5.0 + 0.5)),
|
|
|
int(239 * (randf() / 5.0 + 0.5)))
|
|
|
+ else:
|
|
|
+ color = KINDS[kind].color
|
|
|
+ _rebuild_shape()
|
|
|
+ set_physics_process(kind == Kind.GHOST or kind == Kind.MAGNET)
|
|
|
+
|
|
|
+
|
|
|
+func is_breakable() -> bool:
|
|
|
+ return hp > 0
|
|
|
+
|
|
|
|
|
|
+## Centre of the segment in global coordinates.
|
|
|
+func center() -> Vector2:
|
|
|
+ var middle := (_begin + _end) / 2.0
|
|
|
+ return to_global(Vector2(sin(middle), cos(middle)) * (_inner + _outer) / 2.0)
|
|
|
+
|
|
|
+
|
|
|
+## A ball hit the segment. Applies the segment's effect on the ball and takes a hit.
|
|
|
+func strike(ball: Ball) -> Hit:
|
|
|
+ if not is_breakable():
|
|
|
+ if ball.superball:
|
|
|
+ ball.superball = false
|
|
|
+ destroy()
|
|
|
+ return Hit.BROKE
|
|
|
+ return Hit.BLOCKED
|
|
|
+ if ball.fire:
|
|
|
+ # Fire burns straight through whatever the segment is.
|
|
|
+ hp = 1
|
|
|
+ return damage()
|
|
|
+ match kind:
|
|
|
+ Kind.ICE:
|
|
|
+ ball.velocity *= ICE_SPEEDUP
|
|
|
+ Kind.RUBBER:
|
|
|
+ ball.velocity *= RUBBER_SPEEDUP
|
|
|
+ var result := damage()
|
|
|
+ if kind == Kind.WATER and result == Hit.DAMAGED:
|
|
|
+ ball.stick_to(self, WATER_STICK_SECONDS, WATER_SLOWDOWN)
|
|
|
+ return result
|
|
|
|
|
|
-## Flashes and fades out, then frees itself. Stops colliding right away.
|
|
|
+
|
|
|
+## Takes one hit from anything (ball, laser, explosion). Unbreakable segments ignore it.
|
|
|
+func damage() -> Hit:
|
|
|
+ if not is_breakable() or collision_layer == 0:
|
|
|
+ return Hit.BLOCKED
|
|
|
+ hp -= 1
|
|
|
+ if hp > 0:
|
|
|
+ _rebuild_shape()
|
|
|
+ return Hit.DAMAGED
|
|
|
+ destroy()
|
|
|
+ return Hit.BROKE
|
|
|
+
|
|
|
+
|
|
|
+## Flashes and fades out. Stops colliding right away. Moss comes back once, after a while.
|
|
|
func destroy() -> void:
|
|
|
collision_layer = 0
|
|
|
+ if kind == Kind.TNT:
|
|
|
+ detonated.emit(self)
|
|
|
var tween := create_tween()
|
|
|
tween.tween_property(self, "_flash", 0.8, BREAK_SECONDS / 3.0)
|
|
|
tween.tween_property(self, "modulate:a", 0.0, BREAK_SECONDS)
|
|
|
tween.parallel().tween_property(self, "scale", Vector2.ONE * 1.04, BREAK_SECONDS)
|
|
|
- tween.tween_callback(queue_free)
|
|
|
+ if kind == Kind.MOSS and not _regrown:
|
|
|
+ tween.tween_interval(MOSS_REGROW_SECONDS)
|
|
|
+ tween.tween_callback(_regrow)
|
|
|
+ else:
|
|
|
+ tween.tween_callback(queue_free)
|
|
|
+
|
|
|
+
|
|
|
+func _regrow() -> void:
|
|
|
+ _regrown = true
|
|
|
+ hp = KINDS[kind].hp
|
|
|
+ _flash = 0.0
|
|
|
+ scale = Vector2.ONE * 0.6
|
|
|
+ var tween := create_tween().set_parallel()
|
|
|
+ tween.tween_property(self, "modulate:a", 1.0, 0.4)
|
|
|
+ tween.tween_property(self, "scale", Vector2.ONE, 0.4)
|
|
|
+ tween.chain().tween_callback(set.bind("collision_layer", Params.LAYER_BLOCKS))
|
|
|
+
|
|
|
+
|
|
|
+func _physics_process(delta: float) -> void:
|
|
|
+ if kind == Kind.GHOST and hp > 0:
|
|
|
+ _ghost_clock = fmod(_ghost_clock + delta, GHOST_SOLID_SECONDS + GHOST_PHASED_SECONDS)
|
|
|
+ var solid := _ghost_clock < GHOST_SOLID_SECONDS
|
|
|
+ collision_layer = Params.LAYER_BLOCKS if solid else 0
|
|
|
+ modulate.a = 1.0 if solid else 0.2
|
|
|
+ elif kind == Kind.MAGNET and collision_layer != 0:
|
|
|
+ var here := center()
|
|
|
+ for ball: Ball in get_tree().get_nodes_in_group("balls"):
|
|
|
+ var offset := here - ball.global_position
|
|
|
+ var dist := offset.length()
|
|
|
+ if dist > 0.0 and dist < MAGNET_RADIUS:
|
|
|
+ ball.velocity += offset / dist * MAGNET_PULL * (1.0 - dist / MAGNET_RADIUS) * delta
|
|
|
+
|
|
|
+
|
|
|
+## Recomputes the drawn and collision outline. Water drains from the outside in as it is hit;
|
|
|
+## ice gets cracks; other multi-hit segments darken.
|
|
|
+func _rebuild_shape() -> void:
|
|
|
+ var outer := _outer
|
|
|
+ var max_hp: int = KINDS[kind].hp
|
|
|
+ if kind == Kind.WATER and hp < max_hp:
|
|
|
+ outer = _inner + (_outer - _inner) * float(hp) / max_hp
|
|
|
+ _polygon = _round_corners(_sector(_inner + GAP / 2.0, outer - GAP / 2.0, _begin, _end, GAP / 2.0))
|
|
|
+ # Very short segments can leave a sliver too thin to split into convex pieces; skip its
|
|
|
+ # collision rather than let Godot fail on it.
|
|
|
+ var valid := not Geometry2D.decompose_polygon_in_convex(_polygon).is_empty()
|
|
|
+ _collision.set_deferred("polygon", _polygon if valid else PackedVector2Array())
|
|
|
+ _collision.set_deferred("disabled", not valid)
|
|
|
+ _cracks = PackedVector2Array()
|
|
|
+ if kind == Kind.ICE and hp < max_hp:
|
|
|
+ _cracks = _crack_lines()
|
|
|
+ queue_redraw()
|
|
|
+
|
|
|
+
|
|
|
+func _crack_lines() -> PackedVector2Array:
|
|
|
+ var lines := PackedVector2Array()
|
|
|
+ var middle := (_begin + _end) / 2.0
|
|
|
+ var radius := (_inner + _outer) / 2.0
|
|
|
+ var start := Vector2(sin(middle), cos(middle)) * radius
|
|
|
+ for i in 3:
|
|
|
+ var angle := middle + randf_range(-0.6, 0.6) * (_end - _begin) / 2.0
|
|
|
+ var r := lerpf(_inner + GAP, _outer - GAP, randf())
|
|
|
+ lines.append(start)
|
|
|
+ lines.append(Vector2(sin(angle), cos(angle)) * r)
|
|
|
+ return lines
|
|
|
|
|
|
|
|
|
func _draw() -> void:
|
|
|
if _polygon.size() < 3:
|
|
|
return # segment too short to show at all
|
|
|
- var fill := color.lerp(Color.WHITE, _flash)
|
|
|
+ var fill := color
|
|
|
+ if hp > 0 and hp < KINDS[kind].hp and kind != Kind.WATER and kind != Kind.ICE:
|
|
|
+ fill = fill.darkened(0.25)
|
|
|
+ fill = fill.lerp(Color.WHITE, _flash)
|
|
|
draw_colored_polygon(_polygon, fill)
|
|
|
# Polygons have no antialiasing of their own; a smooth outline softens the edge.
|
|
|
var outline := _polygon.duplicate()
|
|
|
outline.append(_polygon[0])
|
|
|
draw_polyline(outline, fill, 1.0, true)
|
|
|
+ if not _cracks.is_empty():
|
|
|
+ draw_multiline(_cracks, Color(0.2, 0.35, 0.45, 0.9), 1.5, true)
|
|
|
|
|
|
|
|
|
## Ring segment between two radii. [param gap] trims each side by that many pixels,
|