class_name Blocks extends Node2D ## The core plus rings of rotating blocks around it, generated from the level number. ## A segment broke, by any means. signal segment_broken(block: Block) ## Something exploded (TNT or a bomb ball) at this global position. signal exploded(at: Vector2) const BLAST_SCENE := preload("res://scenes/blast.tscn") const SEGMENTS_PER_RING := 10 const TNT_RADIUS := 80.0 const TNT_CHAIN_DELAY := 0.12 # seconds between one TNT going off and the next var core: Core var _rings: Array[Node2D] = [] var _speeds: Array[float] = [] var _frozen_time := 0.0 var _outer_radius := 0.0 var _breakable_total := 0 var _breakable_left := 0 ## Builds the level's rings. Rings are made thinner when needed so they all fit within ## [param max_radius]; the ring count, and so the difficulty, stays the same. func build(level: int, max_radius: float) -> void: var layout := _layout(level) var size := minf(layout.size, (max_radius - Params.CORE_RADIUS) / layout.rings) for index in range(layout.rings - 1, -1, -1): _add_ring(level, size, index, layout.indestructible_chance) _outer_radius = Params.CORE_RADIUS + layout.rings * size _breakable_left = _breakable_total core = Core.new() add_child(core) ## Stops the rings turning for a while. func freeze(seconds: float) -> void: _frozen_time = maxf(_frozen_time, seconds) func has_unbreakable() -> bool: for ring in _rings: for block: Block in ring.get_children(): if not block.is_breakable() and block.collision_layer != 0: return true return false ## Damages every breakable segment within [param radius] of [param at] (global position). func explode(at: Vector2, radius: float) -> void: var blast: Node2D = BLAST_SCENE.instantiate() blast.radius = radius blast.position = to_local(at) add_child(blast) exploded.emit(at) var query := PhysicsShapeQueryParameters2D.new() var circle := CircleShape2D.new() circle.radius = radius query.shape = circle query.transform = Transform2D(0.0, at) query.collision_mask = Params.LAYER_BLOCKS for hit in get_world_2d().direct_space_state.intersect_shape(query, 64): if hit.collider is Block: (hit.collider as Block).damage() func _physics_process(delta: float) -> void: _pull_balls(delta) if _frozen_time > 0.0: _frozen_time -= delta return for i in _rings.size(): _rings[i].rotation_degrees += _speeds[i] * delta ## The fewer breakable segments are left, the harder the core pulls nearby balls in. func _pull_balls(delta: float) -> void: if _breakable_total == 0 or not is_instance_valid(core) or core.collision_layer == 0: return var strength := Params.CORE_PULL * clampf(1.0 - float(_breakable_left) / _breakable_total, 0.0, 1.0) if strength <= 0.0: return var reach := _outer_radius + Params.CORE_PULL_REACH for ball: Ball in get_tree().get_nodes_in_group("balls"): var offset := global_position - ball.global_position var dist := offset.length() if dist > 0.0 and dist < reach: ball.velocity += offset / dist * strength * delta func _on_broken(block: Block) -> void: _breakable_left -= 1 segment_broken.emit(block) func _on_detonated(block: Block) -> void: var at := block.center() get_tree().create_timer(TNT_CHAIN_DELAY, false, true).timeout.connect(explode.bind(at, TNT_RADIUS)) ## Ring count, ring thickness and indestructible-block chance for a level. All of them only ## ever grow with the level, so no level is easier than the one before it. static func _layout(level: int) -> Dictionary: return { rings = Params.ring_count(level), size = Params.RING_SIZE, indestructible_chance = Params.indestructible_chance(level), } ## Picks the kind of one segment. Special kinds unlock every 50 levels; their share of ## the segments grows with the level, and the newest kind is twice as likely as the others. static func _segment_kind(level: int, indestructible_chance: float) -> Block.Kind: if indestructible_chance >= randf(): return Block.Kind.INDESTRUCTIBLE var unlocked: Array[Block.Kind] = [] for kind: Block.Kind in Block.KINDS: var unlock: int = Block.KINDS[kind].unlock if unlock > 0 and unlock <= level: unlocked.append(kind) if unlocked.is_empty() or randf() >= Params.special_segment_share(level): return Block.Kind.NORMAL var weights: Array[float] = [] for kind in unlocked: weights.append(2.0 if kind == unlocked[-1] else 1.0) var roll: float = randf() * weights.reduce(func(sum: float, w: float) -> float: return sum + w, 0.0) for i in unlocked.size(): roll -= weights[i] if roll <= 0.0: return unlocked[i] return unlocked[-1] func _add_ring(level: int, size: float, index: int, indestructible_chance: float) -> void: var inner := index * size + Params.CORE_RADIUS var outer := (index + 1) * size + Params.CORE_RADIUS # Evenly spaced segment boundaries, each jittered so segments have uneven lengths. var step := TAU / SEGMENTS_PER_RING var bounds: Array[float] = [] for i in SEGMENTS_PER_RING: bounds.append(step * i + randf_range(-1.0, 1.0) * 0.45 * step) bounds.append(bounds[0] + TAU) var ring := Node2D.new() for i in SEGMENTS_PER_RING: var block := Block.new(inner, outer, bounds[i], bounds[i + 1], _segment_kind(level, indestructible_chance)) block.detonated.connect(_on_detonated) block.broken.connect(_on_broken) if block.is_breakable(): _breakable_total += 1 ring.add_child(block) add_child(ring) _rings.append(ring) var max_speed := Params.ring_speed(level) _speeds.append(randf_range(-max_speed, max_speed))