282 lines
9.3 KiB
GDScript
282 lines
9.3 KiB
GDScript
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
|