diff --git a/resources/pickups/drift_diagonal.tres b/resources/pickups/drift_diagonal.tres new file mode 100644 index 0000000..f7daa49 --- /dev/null +++ b/resources/pickups/drift_diagonal.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PickupDriftBehavior" format=3 uid="uid://cdiglwsw46g4y"] + +[ext_resource type="Script" uid="uid://b4lar4f3526b1" path="res://resources/pickups/pickup_drift_behavior.gd" id="1_drift"] + +[resource] +script = ExtResource("1_drift") +drift_type = 3 +wave_amplitude = 100.0 +wave_frequency = 2.0 +drift_speed = 30.0 +diagonal_forward = true +drift_along_x = true +reverse_direction = false +randomize_origin = false diff --git a/resources/pickups/drift_leaf_fall.tres b/resources/pickups/drift_leaf_fall.tres new file mode 100644 index 0000000..bc11619 --- /dev/null +++ b/resources/pickups/drift_leaf_fall.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PickupDriftBehavior" format=3 uid="uid://pickup_drift_leaf"] + +[ext_resource type="Script" uid="uid://pickup_drift_uid" path="res://resources/pickups/pickup_drift_behavior.gd" id="1_drift"] + +[resource] +script = ExtResource("1_drift") +drift_type = 2 +drift_speed = 30.0 +fall_speed = 15.0 +wave_amplitude = 40.0 +wave_frequency = 1.0 +drift_along_x = true +reverse_direction = false +randomize_origin = false diff --git a/resources/pickups/drift_sine_wave.tres b/resources/pickups/drift_sine_wave.tres new file mode 100644 index 0000000..a295e87 --- /dev/null +++ b/resources/pickups/drift_sine_wave.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="PickupDriftBehavior" format=3 uid="uid://cr7lye10sx1q6"] + +[ext_resource type="Script" uid="uid://b4lar4f3526b1" path="res://resources/pickups/pickup_drift_behavior.gd" id="1_drift"] + +[resource] +script = ExtResource("1_drift") +drift_type = 1 +drift_speed = 30.0 +wave_amplitude = 40.0 +wave_frequency = 1.0 +drift_along_x = true +reverse_direction = false +randomize_origin = false diff --git a/resources/pickups/pickup_behavior.gd b/resources/pickups/pickup_behavior.gd new file mode 100644 index 0000000..6dcc2e7 --- /dev/null +++ b/resources/pickups/pickup_behavior.gd @@ -0,0 +1,43 @@ +class_name PickupBehavior extends Resource + +## Base resource that defines how a pickup moves and looks on screen. +## Assign to [member pickup_behavior] on a PickupBehaviorController. + +@export_group("Animation") +## Whether the pickup bobs up and down in place. +@export var bob_enabled: bool = true +## Bob oscillation frequency in cycles per second. +@export var bob_frequency: float = 2.0 +## Bob vertical amplitude in pixels (peak-to-center). +@export var bob_amplitude: float = 4.0 + +## Whether the pickup subtly shakes (jitter) in place. +@export var shake_enabled: bool = false +## Shake frequency in Hz (how fast the jitter oscillates). +@export var shake_frequency: float = 8.0 +## Shake amplitude in pixels (peak-to-center). +@export var shake_amplitude: float = 1.5 + +## Optional fade-in duration (0 = no fade). +@export var fade_in_duration: float = 0.0 +## Optional fade-out duration when the pickup is about to be collected +## (used by controller; 0 = no fade). +@export var fade_out_duration: float = 0.0 + +var _elapsed: float = 0.0 + + +func start() -> void: + _elapsed = 0.0 + + +func process(delta: float) -> float: + # Returns the current opacity factor (0..1) based on fade timers. + _elapsed += delta + + var target_alpha: float = 1.0 + + if fade_in_duration > 0 and _elapsed < fade_in_duration: + target_alpha = clampf(_elapsed / fade_in_duration, 0.0, 1.0) + + return target_alpha diff --git a/resources/pickups/pickup_behavior.gd.uid b/resources/pickups/pickup_behavior.gd.uid new file mode 100644 index 0000000..3d10751 --- /dev/null +++ b/resources/pickups/pickup_behavior.gd.uid @@ -0,0 +1 @@ +uid://d0l8e2ymk0lx1 diff --git a/resources/pickups/pickup_drift_behavior.gd b/resources/pickups/pickup_drift_behavior.gd new file mode 100644 index 0000000..11c6f4f --- /dev/null +++ b/resources/pickups/pickup_drift_behavior.gd @@ -0,0 +1,47 @@ +class_name PickupDriftBehavior extends Resource + +## Base resource that defines the global travel pattern of a pickup across +## the screen. Assign to [member drift_behavior] on a PickupBehaviorController. + +enum DriftType { + STATIONARY, ## Does not move (aside from bob/shake). + SINE_WAVE, ## Travels in a sine curve. + LEAF_FALL, ## Drifts downward with gentle horizontal sway. + DIAGONAL, ## Moves diagonally in one direction. +} + +@export_group("Drift") +## The drift pattern to apply. +@export var drift_type: DriftType = DriftType.STATIONARY + +## Horizontal speed in pixels per second (applies to SINE_WAVE, LEAF_FALL). +@export var drift_speed: float = 30.0 +## Vertical fall speed in pixels per second (applies to LEAF_FALL). +@export var fall_speed: float = 15.0 + +## Horizontal wave amplitude in pixels (applies to SINE_WAVE, LEAF_FALL). +@export var wave_amplitude: float = 40.0 +## Wave frequency in cycles per second (applies to SINE_WAVE, LEAF_FALL). +@export var wave_frequency: float = 1.0 + +## Diagonal direction: +X forward, -X backward (applies to DIAGONAL). +@export var diagonal_forward: bool = true + +## Main drift axis for SINE_WAVE and LEAF_FALL. +## true = horizontal travel (x), false = vertical travel (y). +@export var drift_along_x: bool = true + +## Reverse the primary direction (left/right or up/down). +@export var reverse_direction: bool = false + +## Randomize the perpendicular axis origin (y if horizontal, x if vertical). +@export var randomize_origin: bool = false + +## Phase offset in radians — randomize per-instance for variety. +@export var phase_offset: float = 0.0 + +var _initial_x: float = 0.0 + + +func reset(initial_position: Vector2) -> void: + _initial_x = initial_position.x diff --git a/resources/pickups/pickup_drift_behavior.gd.uid b/resources/pickups/pickup_drift_behavior.gd.uid new file mode 100644 index 0000000..f097edf --- /dev/null +++ b/resources/pickups/pickup_drift_behavior.gd.uid @@ -0,0 +1 @@ +uid://b4lar4f3526b1 diff --git a/resources/pickups/rare_powerup.tres b/resources/pickups/rare_powerup.tres new file mode 100644 index 0000000..007d667 --- /dev/null +++ b/resources/pickups/rare_powerup.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="PickupBehavior" format=3 uid="uid://pickup_rare_powerup"] + +[ext_resource type="Script" uid="uid://pickup_behavior_uid" path="res://resources/pickups/pickup_behavior.gd" id="1_behavior"] + +[resource] +script = ExtResource("1_behavior") +bob_enabled = true +bob_frequency = 3.5 +bob_amplitude = 6.0 +shake_enabled = true +shake_frequency = 12.0 +shake_amplitude = 1.5 +fade_in_duration = 0.5 diff --git a/resources/pickups/std_powerup.tres b/resources/pickups/std_powerup.tres new file mode 100644 index 0000000..e72a0c0 --- /dev/null +++ b/resources/pickups/std_powerup.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="PickupBehavior" format=3 uid="uid://dflj31qle75ol"] + +[ext_resource type="Script" uid="uid://d0l8e2ymk0lx1" path="res://resources/pickups/pickup_behavior.gd" id="1_behavior"] + +[resource] +script = ExtResource("1_behavior") +shake_enabled = true +fade_in_duration = 0.3 diff --git a/scenes/player.gd b/scenes/player.gd index 12ea90b..281afe9 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -5,7 +5,7 @@ class_name Player extends Area2D @onready var movement_component: MovementComponent = %MovementComponent @onready var shoot_component: ShootComponent = %ShootComponent @onready var weapon_component: WeaponComponent = %WeaponComponent -@onready var powerup_spawner := _find_powerup_spawner() +@onready var powerup_spawner: PowerUpSpawner = _find_powerup_spawner() # Get the viewport size for positioning @onready var screensize = get_viewport().content_scale_size @@ -63,7 +63,8 @@ func _on_help_box_area_entered(area: Area2D) -> void: return var weapon_shot: WeaponShot = area.get_meta("weapon_shot") weapon_component.collect_weapon(weapon_shot) - powerup_spawner.pickup_collected() + if powerup_spawner != null: + powerup_spawner.pickup_collected() area.queue_free() diff --git a/scenes/player.tscn b/scenes/player.tscn index 165f5b3..657d790 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -100,6 +100,9 @@ size = Vector2(6, 5.75) [node name="Player" type="Area2D" unique_id=652131079] script = ExtResource("1_ur7pv") +can_shoot = null +is_shooting = null +muzzle_flash = null [node name="Ship" type="Sprite2D" parent="." unique_id=1155866924] unique_name_in_owner = true diff --git a/scenes/power_up_spawner.tscn b/scenes/power_up_spawner.tscn index ac4dce4..50d09c3 100644 --- a/scenes/power_up_spawner.tscn +++ b/scenes/power_up_spawner.tscn @@ -8,10 +8,21 @@ [ext_resource type="Resource" uid="uid://ds53sryglmf27" path="res://resources/player_weapon_resources/spread.tres" id="6_mwa7c"] [ext_resource type="Resource" uid="uid://bhc6aja38vyr" path="res://resources/player_weapon_resources/vanguard.tres" id="7_vphcr"] [ext_resource type="Resource" uid="uid://c41aoyka7nfr8" path="res://resources/player_weapon_resources/vanguard+.tres" id="8_g67dp"] +[ext_resource type="Resource" uid="uid://pickup_std_powerup" path="res://resources/pickups/std_powerup.tres" id="9_std"] +[ext_resource type="Resource" uid="uid://pickup_rare_powerup" path="res://resources/pickups/rare_powerup.tres" id="10_rare"] +[ext_resource type="Resource" uid="uid://pickup_drift_sine" path="res://resources/pickups/drift_sine_wave.tres" id="11_sine"] +[ext_resource type="Resource" uid="uid://pickup_drift_leaf" path="res://resources/pickups/drift_leaf_fall.tres" id="12_leaf"] +[ext_resource type="Resource" uid="uid://pickup_drift_diagonal" path="res://resources/pickups/drift_diagonal.tres" id="13_diag"] [node name="PowerUpSpawner" type="Node2D" unique_id=1191463626] script = ExtResource("2_9m3n7") pickup_scene = ExtResource("1_xk8p2") +pickup_behaviors = Array[ExtResource("3_07x44")]( + [ExtResource("9_std"), ExtResource("10_rare")] +) +drift_behaviors = Array[ExtResource("3_07x44")]( + [ExtResource("11_sine"), ExtResource("12_leaf"), ExtResource("13_diag")] +) all_weapon_shots = Array[ExtResource("3_07x44")]( [ExtResource("4_0eev3"), ExtResource("5_qqjui"), ExtResource("6_mwa7c"), ExtResource("7_vphcr"), ExtResource("8_g67dp")] ) diff --git a/scenes/powerup_pickup.tscn b/scenes/powerup_pickup.tscn index 8073dac..e878206 100644 --- a/scenes/powerup_pickup.tscn +++ b/scenes/powerup_pickup.tscn @@ -1,9 +1,16 @@ [gd_scene format=3 uid="uid://dsqfuv86suu17"] +[ext_resource type="Script" uid="uid://pickup_controller_uid" path="res://scripts/pickups/pickup_behavior_controller.gd" id="1_ctrl"] +[ext_resource type="Resource" uid="uid://pickup_behavior_uid" path="res://resources/pickups/std_powerup.tres" id="2_std"] +[ext_resource type="Resource" uid="uid://pickup_drift_uid" path="res://resources/pickups/drift_sine_wave.tres" id="3_drift"] + [sub_resource type="RectangleShape2D" id="RectangleShape2D_mmgg1"] size = Vector2(14, 14) [node name="PowerUpPickup" type="Area2D" unique_id=478708932] +script = ExtResource("1_ctrl") +behavior = ExtResource("2_std") +drift_behavior = ExtResource("3_drift") collision_mask = 0 [node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=347643413] diff --git a/scripts/pickups/pickup_behavior_controller.gd b/scripts/pickups/pickup_behavior_controller.gd new file mode 100644 index 0000000..783e784 --- /dev/null +++ b/scripts/pickups/pickup_behavior_controller.gd @@ -0,0 +1,132 @@ +class_name PickupBehaviorController extends Area2D + +## Controls visual movement (bob, shake) and drift of a pickup. +## Attach to the root Area2D node of your pickup scene. + +@export var behavior: PickupBehavior +@export var drift_behavior: PickupDriftBehavior + + +var _icon_node: TextureRect +var _spawn_position: Vector2 = Vector2.ZERO + + +func _ready() -> void: + # Find the runtime-added icon node. + for child in get_children(): + if child is TextureRect: + _icon_node = child as TextureRect + break + # Capture the initial spawn position. + _spawn_position = global_position + + if behavior != null: + behavior.start() + + +func _process(delta: float) -> void: + if behavior == null or drift_behavior == null: + return + + var alpha: float = behavior.process(delta) + + if _icon_node != null: + _icon_node.visible = alpha > 0.01 + if behavior != null and behavior.fade_in_duration > 0 and _icon_node is CanvasItem: + _icon_node.modulate.a = alpha + + var offset := _compute_animation_offset() + var drift_offset := _compute_drift_offset() + + position = _spawn_position + Vector2(drift_offset.x, drift_offset.y) + + if _icon_node != null: + _icon_node.position = offset + + +func _compute_animation_offset() -> Vector2: + # Returns the bob + shake offset applied to the icon node. + var result := Vector2.ZERO + + if behavior == null: + return result + + # Bob (vertical oscillation). + if behavior.bob_enabled: + var bob_angle := _elapsed_time() * TAU * behavior.bob_frequency + result.y += sin(bob_angle) * behavior.bob_amplitude + + # Shake (subtle jitter). + if behavior.shake_enabled: + var shake_angle := _elapsed_time() * TAU * behavior.shake_frequency + result.x += sin(shake_angle) * behavior.shake_amplitude + + return result + + +func _compute_drift_offset() -> Vector2: + # Returns the global drift position offset from initial spawn position. + if drift_behavior == null: + return Vector2.ZERO + + match drift_behavior.drift_type: + PickupDriftBehavior.DriftType.STATIONARY: + return Vector2.ZERO + + PickupDriftBehavior.DriftType.SINE_WAVE: + var t := _elapsed_time() * drift_behavior.wave_frequency + var travel_speed: float = drift_behavior.drift_speed + if drift_behavior.reverse_direction: + travel_speed *= -1.0 + + var offset := Vector2.ZERO + if drift_behavior.drift_along_x: + # Travels along X; waves on Y. + offset.x = travel_speed * _elapsed_time() + offset.y = sin(t + drift_behavior.phase_offset) * drift_behavior.wave_amplitude + else: + # Travels along Y; waves on X. + offset.y = travel_speed * _elapsed_time() + offset.x = sin(t + drift_behavior.phase_offset) * drift_behavior.wave_amplitude + return offset + + PickupDriftBehavior.DriftType.LEAF_FALL: + var leaf_t := _elapsed_time() * drift_behavior.wave_frequency + var travel_speed: float = drift_behavior.fall_speed + if drift_behavior.reverse_direction: + travel_speed *= -1.0 + + var offset := Vector2.ZERO + if drift_behavior.drift_along_x: + # Travels along X; waves on Y. + offset.x = travel_speed * _elapsed_time() + offset.y = sin(leaf_t + drift_behavior.phase_offset) * drift_behavior.wave_amplitude + else: + # Travels along Y; waves on X. + offset.y = travel_speed * _elapsed_time() + offset.x = sin(leaf_t + drift_behavior.phase_offset) * drift_behavior.wave_amplitude + return offset + + PickupDriftBehavior.DriftType.DIAGONAL: + var dir: float = 1.0 if drift_behavior.diagonal_forward else -1.0 + if drift_behavior.reverse_direction: + dir *= -1.0 + + var offset := Vector2.ZERO + if drift_behavior.drift_along_x: + # Travels along X; secondary on Y. + offset.x = dir * drift_behavior.drift_speed * _elapsed_time() + offset.y = dir * (drift_behavior.drift_speed * 0.5) * _elapsed_time() + else: + # Travels along Y; secondary on X. + offset.y = dir * drift_behavior.drift_speed * _elapsed_time() + offset.x = dir * (drift_behavior.drift_speed * 0.5) * _elapsed_time() + return offset + + return Vector2.ZERO + + +func _elapsed_time() -> float: + if behavior == null: + return 0.0 + return behavior._elapsed diff --git a/scripts/pickups/pickup_behavior_controller.gd.uid b/scripts/pickups/pickup_behavior_controller.gd.uid new file mode 100644 index 0000000..06e365b --- /dev/null +++ b/scripts/pickups/pickup_behavior_controller.gd.uid @@ -0,0 +1 @@ +uid://c4a7husvaq2eo diff --git a/scripts/power_up_spawner.gd b/scripts/power_up_spawner.gd index 716eecf..65c62ab 100644 --- a/scripts/power_up_spawner.gd +++ b/scripts/power_up_spawner.gd @@ -9,18 +9,44 @@ class_name PowerUpSpawner extends Node2D @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: @@ -29,6 +55,12 @@ func _on_spawn_timer_timeout() -> void: _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(): @@ -59,26 +91,51 @@ func _spawn_normal_pickup() -> void: func _spawn_test_pickups() -> void: - for weapon: WeaponShot in all_weapon_shots: - if weapon == null or weapon.shot_name.is_empty(): - continue + # In test mode: pick ONE random weapon and spawn a single pickup. + if all_weapon_shots.is_empty(): + return - var pickup := _spawn_pickup(weapon) - if not is_instance_valid(pickup): - continue # skipped — no valid icon + var chosen_weapon: WeaponShot = all_weapon_shots[randi() % all_weapon_shots.size()] + if chosen_weapon == null or chosen_weapon.shot_name.is_empty(): + return - # Random position within viewport, with edge padding. - var margin: float = 15.0 + 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) - _pickups_spawned += 1 - _pickups_remaining = _pickups_spawned + add_child(pickup) + _active_pickups.append(pickup) + _pickups_spawned += 1 + _pickups_remaining = _pickups_spawned - # Pause the timer while pickups are on screen; resume when all gone. + # Pause the timer while pickup is on screen; resume when gone or expired. if spawn_timer != null: spawn_timer.stop() @@ -92,12 +149,16 @@ func pickup_collected() -> void: var wc: WeaponComponent = level.get_node_or_null("WeaponComponent") if wc != null: wc.test_mode = test_mode - # Decrement counter; resume timer once all pickups are gone. - _pickups_remaining = max(0, _pickups_remaining - 1) - if _pickups_remaining <= 0: - if spawn_timer != null: - spawn_timer.start() - _pickups_spawned = 0 + # 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: @@ -123,4 +184,99 @@ func _spawn_pickup(weapon: WeaponShot) -> Area2D: 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