class_name EnemyManager extends Node2D @export var max_enemies: int @export var spawn_rate: float @export var target: CollisionObject2D @export var camera: Camera2D @onready var timer: Timer = $Timer const ENEMY_RAT = preload("res://scenes/enemies/enemy_rat.tscn") const ENEMY_BAT = preload("res://scenes/enemies/enemy_bat.tscn") const ENEMY_SLIME_SMALL = preload("res://scenes/enemies/enemy_slime_small.tscn") const SLIME_COLOR_VARIATIONS: Array[Color] = [Color.CHARTREUSE, Color.FUCHSIA, Color.DARK_ORANGE] var _elapsed_time: float = 0.0 func _ready() -> void: timer.wait_time = 1 / spawn_rate timer.start() GlobalConst.sig_stop_spawning.connect(_on_stop_spawning) GlobalConst.sig_set_spawn_rate.connect(_on_set_spawn_rate) func _physics_process(delta: float) -> void: _elapsed_time += delta func _on_timer_timeout() -> void: _on_set_spawn_rate(1.0 + (_elapsed_time / 60.0) ** 2) var enemies = get_tree().get_nodes_in_group(GlobalConst.GROUP_ENEMY) GlobalConst.sig_debug_stats_set.emit("enemy_count", "%s" % len(enemies)) var next_enemy: PackedScene match randi() % 3: 0: next_enemy = ENEMY_BAT 1: next_enemy = ENEMY_RAT 2: next_enemy = ENEMY_SLIME_SMALL if len(enemies) < max_enemies: var new_enemy = next_enemy.instantiate() new_enemy.position = _get_spawn_pos() new_enemy.target = target if randf() < 0.1: new_enemy.enemy_rarity = GlobalConst.Rarity.RARE if randf() < 0.1: new_enemy.enemy_rarity = GlobalConst.Rarity.EPIC if is_instance_of(new_enemy, EnemySlimeSmall): var slime_color: Color = SLIME_COLOR_VARIATIONS.pick_random() new_enemy.color = slime_color add_child(new_enemy) func _get_spawn_pos() -> Vector2: var rect = _get_camera_rect() var side = randi() % 4 var margin = 50 var pos: Vector2 match side: 0: # Top pos = Vector2( randf_range(rect.position.x, rect.position.x + rect.size.x), rect.position.y - margin ) 1: # Bottom pos = Vector2( randf_range(rect.position.x, rect.position.x + rect.size.x), rect.position.y + rect.size.y + margin ) 2: # Left pos = Vector2( rect.position.x - margin, randf_range(rect.position.y, rect.position.y + rect.size.y) ) 3: # Right pos = Vector2( rect.position.x + rect.size.x + margin, randf_range(rect.position.y, rect.position.y + rect.size.y) ) if !_is_pos_valid(pos): pos = _get_spawn_pos() return pos func _is_pos_valid(pos: Vector2) -> bool: var space = get_world_2d().direct_space_state var parameters = PhysicsPointQueryParameters2D.new() parameters.collide_with_areas = false parameters.collide_with_bodies = true parameters.collision_mask = 1 parameters.position = pos var result = space.intersect_point(parameters, 1) return result.size() == 0 func _get_camera_rect() -> Rect2: var viewport_size = camera.get_viewport_rect().size var half_size = viewport_size * 0.5 var top_left = camera.global_position - half_size return Rect2(top_left, viewport_size) func _on_stop_spawning(val: bool): if val: timer.stop() elif timer.is_stopped(): timer.start() func _on_set_spawn_rate(val: float): timer.stop() timer.wait_time = 1.0 / val timer.start()