class_name PowerUpSpawner extends Node2D @export var spawn_interval: float = 5.0 @export var test_mode: bool = true # PackedScene for the pickup template — configure in the scene editor. @export var pickup_scene: PackedScene @onready var screensize: Vector2 = get_viewport().content_scale_size @onready var spawn_timer: Timer = $SpawnTimer # Optional behavior resources to assign randomly on spawn. @export var pickup_behaviors: Array[PickupBehavior] = [] # All weapon resources to spawn as pickups. # Populate in the editor with your weapon_shot resources. @export var all_weapon_shots: Array[WeaponShot] = [] # Optional drift behaviors to assign randomly on spawn. # Leave empty to use whatever is baked into the pickup scene. @export var drift_behaviors: Array[PickupDriftBehavior] = [] # How long a pickup can linger off-screen before being cleaned up. @export var offscreen_timeout: float = 5.0 # How long to wait after a pickup is collected before spawning the next one. @export var cooldown_after_collection: float = 5.0 var _pickups_spawned: int = 0 var _pickups_remaining: int = 0 # Tracks active pickups so we can check off-screen timeouts. var _active_pickups: Array[Area2D] = [] # Cooldown state for post-collection pause. var _cooldown_timer: Timer var _in_cooldown: bool = false func _ready() -> void: if spawn_timer == null: return spawn_timer.wait_time = spawn_interval # Create a one-shot cooldown timer. _cooldown_timer = Timer.new() _cooldown_timer.one_shot = true _cooldown_timer.timeout.connect(_on_cooldown_finished) add_child(_cooldown_timer) func _on_spawn_timer_timeout() -> void: if test_mode: _spawn_test_pickups() else: _spawn_normal_pickup() func _on_cooldown_finished() -> void: _in_cooldown = false if spawn_timer != null and test_mode: spawn_timer.start() func _spawn_normal_pickup() -> void: # Spawn a single random pickup at a position near the player. if all_weapon_shots.is_empty(): return var pickup := _spawn_pickup( all_weapon_shots[randi() % all_weapon_shots.size()] ) if pickup == null or not is_instance_valid(pickup): return # Position: centered horizontally, above the player with vertical padding. var player_node := get_node_or_null("../Player") if player_node != null: pickup.position = Vector2( randf_range(40.0, screensize.x - 40.0), player_node.position.y - randf_range(80.0, 200.0) ) else: pickup.position = Vector2( randf_range(40.0, screensize.x - 40.0), randf_range(30.0, screensize.y * 0.4) ) add_child(pickup) _pickups_spawned += 1 _pickups_remaining = max(_pickups_remaining, _pickups_spawned) func _spawn_test_pickups() -> void: # In test mode: pick ONE random weapon and spawn a single pickup. if all_weapon_shots.is_empty(): return var chosen_weapon: WeaponShot = all_weapon_shots[randi() % all_weapon_shots.size()] if chosen_weapon == null or chosen_weapon.shot_name.is_empty(): return var pickup := _spawn_pickup(chosen_weapon) if not is_instance_valid(pickup): return # Random position within viewport, with edge padding. var margin: float = 15.0 if _should_randomize_origin(pickup): # Randomize the perpendicular axis so pickups don't all start at one edge. var drift := _get_pickup_drift(pickup) if drift != null: var offset := Vector2.ZERO if drift.drift_along_x: # Travels along X; randomize Y origin. offset.x = randf_range(margin, screensize.x - margin) offset.y = 0.0 else: # Travels along Y; randomize X origin. offset.x = 0.0 offset.y = randf_range(margin, screensize.y - margin) pickup.position = offset else: pickup.position = Vector2( randf_range(margin, screensize.x - margin), randf_range(margin, screensize.y - margin) ) else: pickup.position = Vector2( randf_range(margin, screensize.x - margin), randf_range(margin, screensize.y - margin) ) add_child(pickup) _active_pickups.append(pickup) _pickups_spawned += 1 _pickups_remaining = _pickups_spawned # Pause the timer while pickup is on screen; resume when gone or expired. if spawn_timer != null: spawn_timer.stop() func pickup_collected() -> void: # Called by the player when a pickup is collected. # Sync WeaponComponent's test_mode so tab cycling stays independent of # pickups when in test mode, and normal behavior is restored otherwise. var level: Node2D = get_parent() as Node2D if level != null: var wc: WeaponComponent = level.get_node_or_null("WeaponComponent") if wc != null: wc.test_mode = test_mode # Remove from active list and start cooldown. _active_pickups.clear() if spawn_timer != null: spawn_timer.stop() _start_cooldown() func _process(delta: float) -> void: if test_mode: _update_active_pickups(delta) func _spawn_pickup(weapon: WeaponShot) -> Area2D: # Validate that the weapon has a pickup icon configured. if weapon.pickup_icon == null: printerr("[PowerUpSpawner] No pickup icon set for weapon: ", weapon.shot_name) var dead := Area2D.new() # return a dead node — caller skips it dead.queue_free() return dead # Instantiate the scene template and attach weapon metadata. var pickup := pickup_scene.instantiate() as Area2D if pickup == null: printerr("[PowerUpSpawner] Failed to instantiate pickup scene.") return null pickup.set_meta("weapon_shot", weapon) # Add the icon node at runtime (scene has no TextureRect to avoid null-dependency issues). var icon := TextureRect.new() icon.texture = weapon.pickup_icon icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.size = Vector2(16, 16) pickup.add_child(icon) # Assign a random behavior for visual variety. _apply_random_behavior(pickup) # Assign a random drift behavior for visual variety. _apply_random_drift(pickup) return pickup func _cleanup_pickup(pickup: Area2D) -> void: # Remove from active list if present. for i in _active_pickups.size(): if _active_pickups[i] == pickup: _active_pickups.remove_at(i) break pickup.queue_free() # Trigger cooldown/respawn when a pickup drifts off-screen. _start_cooldown() func _update_active_pickups(_delta: float) -> void: # Check each active pickup for off-screen timeout. var viewport_rect := get_viewport_rect().size var margin: float = 50.0 # how far off-screen counts as "gone" var to_remove: Array[Area2D] = [] for pickup in _active_pickups: if not is_instance_valid(pickup): continue # Simple off-screen check: outside viewport + margin. if (pickup.position.x < -margin or pickup.position.x > viewport_rect.x + margin or pickup.position.y < -margin or pickup.position.y > viewport_rect.y + margin): to_remove.append(pickup) for bad_pickup in to_remove: _cleanup_pickup(bad_pickup) func _start_cooldown() -> void: if offscreen_timeout <= 0 or cooldown_after_collection <= 0: return _in_cooldown = true _cooldown_timer.wait_time = cooldown_after_collection _cooldown_timer.start() func _apply_random_behavior(pickup: Area2D) -> void: if pickup_behaviors.is_empty(): return var controller := pickup.get_node_or_null("PickupBehaviorController") if controller == null: return var chosen: PickupBehavior = pickup_behaviors[randi() % pickup_behaviors.size()].duplicate() as PickupBehavior if chosen != null: controller.behavior = chosen func _apply_random_drift(pickup: Area2D) -> void: # Randomly assign a drift behavior to the spawned pickup instance. if drift_behaviors.is_empty(): return var controller := pickup.get_node_or_null("PickupBehaviorController") if controller == null: return var chosen: PickupDriftBehavior = drift_behaviors[randi() % drift_behaviors.size()].duplicate() as PickupDriftBehavior if chosen == null: return # Randomize phase offset for per-instance variety. chosen.phase_offset = randf_range(0.0, TAU) # Randomize axis (horizontal vs vertical travel). chosen.drift_along_x = randf() > 0.5 # Randomize direction (forward vs reverse). chosen.reverse_direction = randf() > 0.5 # Randomize perpendicular origin. chosen.randomize_origin = randf() > 0.5 # When randomizing origin, zero the phase so the wave starts at center (0 offset). if chosen.randomize_origin: chosen.phase_offset = 0.0 controller.drift_behavior = chosen func _get_pickup_drift(pickup: Area2D) -> PickupDriftBehavior: var controller := pickup.get_node_or_null("PickupBehaviorController") if controller == null: return null return controller.drift_behavior func _should_randomize_origin(pickup: Area2D) -> bool: var drift := _get_pickup_drift(pickup) return drift != null and drift.randomize_origin