class_name Block extends AnimatableBody2D ## One arc-shaped segment of a ring around the core. 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 var color := Params.COLOR_BLUE var _polygon := PackedVector2Array() ## 0 to 1: how far the tile is washed towards white while breaking. var _flash := 0.0: set(value): _flash = value queue_redraw() func _init(inner: float, outer: float, begin: float, end: float, is_indestructible: bool) -> void: sync_to_physics = false collision_layer = Params.LAYER_BLOCKS collision_mask = 0 indestructible = is_indestructible # 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: color = Color8( int(102 * (randf() / 5.0 + 0.5)), int(217 * (randf() / 5.0 + 0.5)), int(239 * (randf() / 5.0 + 0.5))) ## Flashes and fades out, then frees itself. Stops colliding right away. func destroy() -> void: collision_layer = 0 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) func _draw() -> void: if _polygon.size() < 3: return # segment too short to show at all var fill := color.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) ## Ring segment between two radii. [param gap] trims each side by that many pixels, ## so the space between neighbours has the same width at every radius. An edge too short ## to trim collapses to a point, turning the segment into a wedge. static func _sector(inner: float, outer: float, begin: float, end: float, gap: float) -> PackedVector2Array: var steps := maxi(2, int((end - begin) * sqrt(outer) / 1.5)) return _trimmed_arc(outer, begin, end, gap, steps) + _trimmed_arc(inner, end, begin, -gap, steps) static func _trimmed_arc(radius: float, from: float, to: float, gap: float, steps: int) -> PackedVector2Array: var trim := gap / radius if (to - from - 2.0 * trim) * signf(to - from) <= 0.0: var middle := (from + to) / 2.0 return PackedVector2Array([Vector2(sin(middle), cos(middle)) * radius]) return _arc(radius, from + trim, to - trim, steps) ## Shrinks then regrows the polygon with round joins, which rounds its corners. static func _round_corners(polygon: PackedVector2Array) -> PackedVector2Array: var shrunk := Geometry2D.offset_polygon(polygon, -CORNER) if shrunk.is_empty(): return polygon var rounded := Geometry2D.offset_polygon(shrunk[0], CORNER, Geometry2D.JOIN_ROUND) return polygon if rounded.is_empty() else _without_duplicates(rounded[0]) ## offset_polygon can emit repeated points; zero-length edges break convex decomposition. static func _without_duplicates(polygon: PackedVector2Array) -> PackedVector2Array: var result := PackedVector2Array() for point in polygon: if result.is_empty() or not point.is_equal_approx(result[-1]): result.append(point) if result.size() > 1 and result[0].is_equal_approx(result[-1]): result.remove_at(result.size() - 1) return result static func _arc(radius: float, from: float, to: float, steps: int) -> PackedVector2Array: var points := PackedVector2Array() for i in steps + 1: var angle := lerpf(from, to, float(i) / steps) points.append(Vector2(sin(angle), cos(angle)) * radius) return points