From 7ffb93963f94b673755f61ff4006672fed70a76f Mon Sep 17 00:00:00 2001 From: henry Date: Mon, 29 Jun 2026 22:12:28 +0100 Subject: [PATCH 1/8] Updated cycle testing logic and various other behaviours for enhanced system. --- resources/player_weapon_resources/spread.tres | 2 + resources/player_weapon_resources/stock.tres | 2 + resources/player_weapon_resources/tri.tres | 2 + .../player_weapon_resources/vanguard+.tres | 2 + .../player_weapon_resources/vanguard.tres | 2 + scenes/player.tscn | 6 +- scripts/notification_display.gd | 3 + scripts/shoot_component.gd | 41 ++-- scripts/weapon_component.gd | 191 ++++++++++++++++-- scripts/weapon_shot.gd | 17 ++ 10 files changed, 237 insertions(+), 31 deletions(-) diff --git a/resources/player_weapon_resources/spread.tres b/resources/player_weapon_resources/spread.tres index bb4794c..dc6b432 100644 --- a/resources/player_weapon_resources/spread.tres +++ b/resources/player_weapon_resources/spread.tres @@ -15,4 +15,6 @@ horizontal_offset = 8.25 spread_angle = 25.0 stagger_offset = 0.5 stagger_animation = true +power_level = 1 +force_replace_all = true metadata/_custom_type_script = "uid://7n1itonn35fm" diff --git a/resources/player_weapon_resources/stock.tres b/resources/player_weapon_resources/stock.tres index 79f8f83..41c3bb3 100644 --- a/resources/player_weapon_resources/stock.tres +++ b/resources/player_weapon_resources/stock.tres @@ -12,4 +12,6 @@ damage = 2 speed = 650 horizontal_offset = 12.0 stagger_offset = 0.0 +power_level = 1 +force_replace_all = true metadata/_custom_type_script = "uid://7n1itonn35fm" diff --git a/resources/player_weapon_resources/tri.tres b/resources/player_weapon_resources/tri.tres index c16b614..d13bcc6 100644 --- a/resources/player_weapon_resources/tri.tres +++ b/resources/player_weapon_resources/tri.tres @@ -14,4 +14,6 @@ projectiles = 3 horizontal_offset = 12.5 stagger_offset = 0.35 stagger_animation = true +power_level = 1 +force_replace_all = true metadata/_custom_type_script = "uid://7n1itonn35fm" diff --git a/resources/player_weapon_resources/vanguard+.tres b/resources/player_weapon_resources/vanguard+.tres index 1e62887..b8b46ab 100644 --- a/resources/player_weapon_resources/vanguard+.tres +++ b/resources/player_weapon_resources/vanguard+.tres @@ -18,4 +18,6 @@ compensate_spawn_geometry = true stagger_animation = true follow_angle = true groups = Array[ExtResource("2_qeb4b")]([ExtResource("3_qeb4b")]) +power_level = 1 +force_replace_all = true metadata/_custom_type_script = "uid://7n1itonn35fm" diff --git a/resources/player_weapon_resources/vanguard.tres b/resources/player_weapon_resources/vanguard.tres index 006c9a5..0970935 100644 --- a/resources/player_weapon_resources/vanguard.tres +++ b/resources/player_weapon_resources/vanguard.tres @@ -14,4 +14,6 @@ projectiles = 5 horizontal_offset = 8.25 stagger_offset = 0.5 stagger_animation = true +power_level = 1 +force_replace_all = true metadata/_custom_type_script = "uid://7n1itonn35fm" diff --git a/scenes/player.tscn b/scenes/player.tscn index c240cfb..a39f299 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -10,12 +10,12 @@ [ext_resource type="Script" uid="uid://ylmao2ndp22y" path="res://scripts/weapon_component.gd" id="7_d2wvv"] [ext_resource type="Resource" uid="uid://b75ae840k03dy" path="res://resources/player_weapon_resources/stock.tres" id="8_stock"] [ext_resource type="Resource" uid="uid://bhc6aja38vyr" path="res://resources/player_weapon_resources/vanguard.tres" id="9_ur7pv"] -[ext_resource type="Script" uid="uid://znfh8unmdxkvv" path="res://scripts/weapon_fire_helper.gd" id="15_firehelper"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="10_d2wvv"] [ext_resource type="Resource" uid="uid://cck3gmnhu5agc" path="res://resources/player_weapon_resources/tri.tres" id="11_3v2ag"] [ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects_component.gd" id="12_effcomp"] [ext_resource type="Resource" uid="uid://ds53sryglmf27" path="res://resources/player_weapon_resources/spread.tres" id="13_f1ej7"] [ext_resource type="Resource" uid="uid://c41aoyka7nfr8" path="res://resources/player_weapon_resources/vanguard+.tres" id="14_oprun"] +[ext_resource type="Script" uid="uid://bpcv2ojtyityc" path="res://scripts/weapon_fire_helper.gd" id="15_firehelper"] [sub_resource type="AtlasTexture" id="AtlasTexture_tuyoq"] atlas = ExtResource("5_qlg0r") @@ -133,7 +133,7 @@ position = Vector2(0, 3) shape = SubResource("RectangleShape2D_tuyoq") debug_color = Color(0, 1, 0, 0.41960785) -[node name="HitBox" type="CollisionShape2D" parent="." unique_id=485826453] +[node name="HitBox" type="CollisionShape2D" parent="." unique_id=484826453] position = Vector2(0, 2) shape = SubResource("RectangleShape2D_dqkch") debug_color = Color(0.9843137, 0, 0, 0.80784315) @@ -164,7 +164,7 @@ script = ExtResource("6_y4r1p") unique_name_in_owner = true script = ExtResource("7_d2wvv") weapon_data = ExtResource("8_stock") -available_weapons = Array[ExtResource("10_d2wvv")]([ExtResource("8_stock"), ExtResource("11_3v2ag"), ExtResource("9_ur7pv"), ExtResource("13_f1ej7"), ExtResource("14_oprun")]) +available_weapons = Array[ExtResource("10_d2wvv")]([ExtResource("8_stock"), ExtResource("11_3v2ag"), ExtResource("13_f1ej7"), ExtResource("9_ur7pv"), ExtResource("14_oprun")]) weapon_change_flash = SubResource("Resource_jej6c") metadata/_custom_type_script = "uid://ylmao2ndp22y" diff --git a/scripts/notification_display.gd b/scripts/notification_display.gd index 4dbbc48..b401cc4 100644 --- a/scripts/notification_display.gd +++ b/scripts/notification_display.gd @@ -33,6 +33,9 @@ func show_notification(weapon_name: String, player_node: Node2D, player_sprite_s var icon_path = "res://graphics/" + weapon_name.to_lower() + ".png" if ResourceLoader.exists(icon_path): icon.texture = load(icon_path) + elif ResourceLoader.exists("res://graphics/stock.png"): + # Fallback to stock icon when no dedicated icon exists + icon.texture = load("res://graphics/stock.png") else: icon.texture = null diff --git a/scripts/shoot_component.gd b/scripts/shoot_component.gd index bf783c1..0fc9234 100644 --- a/scripts/shoot_component.gd +++ b/scripts/shoot_component.gd @@ -5,13 +5,24 @@ class_name ShootComponent extends Node @onready var ship: Sprite2D = get_node("../Ship") @onready var fire_helper: WeaponFireHelper = %WeaponFireHelper -func shoot(): - var weapon_data = weapon_component.weapon_data - var current_time = Time.get_ticks_msec() / 1000.0 +func shoot() -> void: + var active_weapons: Array[WeaponShot] = weapon_component.get_active_weapons() + if active_weapons.is_empty(): + return - if player.travel > weapon_data.projectile_vertical_spacing: - _shoot_standard(weapon_data, current_time) - _shoot_grouped(weapon_data, current_time) + var current_time: float = Time.get_ticks_msec() / 1000.0 + + # Burst gating: find the minimum projectile_vertical_spacing among all + # active weapons. This is the most restrictive timing threshold. + var min_spacing: float = INF + for w: WeaponShot in active_weapons: + if w.projectile_vertical_spacing < min_spacing: + min_spacing = w.projectile_vertical_spacing + + if player.travel > min_spacing: + for w: WeaponShot in active_weapons: + _shoot_standard(w, current_time) + _shoot_grouped(w, current_time) # Reset travel after firing to prevent multiple bursts per tap player.travel = 0.0 @@ -26,12 +37,12 @@ func _get_spawn_position(weapon_data: WeaponShot) -> Vector2: sprite_height = ship.texture.get_height() # Top edge of the sprite in world space, then offset by origin. - var top_edge = ship.global_position.y - (sprite_height / 2.0) + var top_edge: float = ship.global_position.y - (sprite_height / 2.0) return Vector2(ship.global_position.x, top_edge + weapon_data.origin) # Standard projectile logic (delegates to WeaponFireHelper) -func _shoot_standard(weapon_data: WeaponShot, current_time: float): +func _shoot_standard(weapon_data: WeaponShot, current_time: float) -> void: var total_projectiles: int = weapon_data.projectiles if total_projectiles > 0: @@ -40,8 +51,8 @@ func _shoot_standard(weapon_data: WeaponShot, current_time: float): get_tree().root.add_child(bullet) # Delegate math to the helper - var spawn_position = _get_spawn_position(weapon_data) - var props = fire_helper.compute_standard_props(weapon_data, b, spawn_position, current_time) + var spawn_position: Vector2 = _get_spawn_position(weapon_data) + var props: Dictionary = fire_helper.compute_standard_props(weapon_data, b, spawn_position, current_time) bullet.position = props.position bullet.angle = props.angle bullet.time_offset = props.time_offset @@ -56,15 +67,15 @@ func _shoot_standard(weapon_data: WeaponShot, current_time: float): # Grouped projectile logic (delegates to WeaponFireHelper) -func _shoot_grouped(weapon_data: WeaponShot, current_time: float): +func _shoot_grouped(weapon_data: WeaponShot, current_time: float) -> void: for group: WeaponProjectileGroup in weapon_data.groups.filter(func(g): return g != null): for b in range(group.bullet_count): var bullet := weapon_data.bullet_scene.instantiate() as Area2D get_tree().root.add_child(bullet) # Apply computed properties for the original bullet - var spawn_position = _get_spawn_position(weapon_data) - var props = fire_helper.compute_grouped_props(group, weapon_data, b, spawn_position, current_time, false) + var spawn_position: Vector2 = _get_spawn_position(weapon_data) + var props: Dictionary = fire_helper.compute_grouped_props(group, weapon_data, b, spawn_position, current_time, false) bullet.position = props.position bullet.angle = props.angle bullet.time_offset = props.time_offset @@ -79,8 +90,8 @@ func _shoot_grouped(weapon_data: WeaponShot, current_time: float): get_tree().root.add_child(mirror_bullet) # Apply computed properties for the mirrored bullet - var mirror_spawn = _get_spawn_position(weapon_data) - var mirror_props = fire_helper.compute_grouped_props(group, weapon_data, b, mirror_spawn, current_time, true) + var mirror_spawn: Vector2 = _get_spawn_position(weapon_data) + var mirror_props: Dictionary = fire_helper.compute_grouped_props(group, weapon_data, b, mirror_spawn, current_time, true) mirror_bullet.position = mirror_props.position mirror_bullet.angle = mirror_props.angle mirror_bullet.time_offset = mirror_props.time_offset diff --git a/scripts/weapon_component.gd b/scripts/weapon_component.gd index 1442102..63a916b 100644 --- a/scripts/weapon_component.gd +++ b/scripts/weapon_component.gd @@ -4,41 +4,184 @@ class_name WeaponComponent extends Node @export var available_weapons: Array[WeaponShot] = [] @export var weapon_change_flash: BaseEffect = null -var _current_weapon_index: int = 0 +var _current_weapon_index = 0 +var _current_power_level = 0 +var _cycled_weapon: WeaponShot = null -@onready var effects_component: EffectComponent = %EffectsComponent +@onready var effects_component: EffectComponent = get_node("EffectsComponent") func _ready() -> void: - # Set the default weapon to be stock, i.e. index 1 if weapon_data and not available_weapons.is_empty(): for i in range(available_weapons.size()): if available_weapons[i] == weapon_data: _current_weapon_index = i + # Initialize the power level from the default weapon so + # shooting works immediately without needing to cycle. + _current_power_level = weapon_data.power_level return + # If available_weapons was populated from the scene but no matching + # weapon_data was found (e.g. all weapons share the same power level), + # cap the power level to the minimum power level across all weapons. + # This ensures initial shooting works even when weapon_data is null or + # mismatches are found. + if not available_weapons.is_empty(): + var min_level: int = 999 + for w: WeaponShot in available_weapons: + if w != null and w.power_level < min_level: + min_level = w.power_level + _current_power_level = min_level + + +func _compute_active_weapons() -> Array[WeaponShot]: + # Filter available weapons by current power level. + var active: Array[WeaponShot] = [] + for weapon: WeaponShot in available_weapons: + if weapon != null and weapon.power_level <= _current_power_level: + active.append(weapon) + return active + + +func get_active_weapons(power_level: int = -1) -> Array[WeaponShot]: + # Return all weapons active at the given power level. + # + # If power_level is negative, uses the internally tracked power level. + # var _level: int = power_level if power_level >= 0 else _current_power_level + # (unused - power_level parameter is ignored by design) + # If the cycled weapon is set, return it exclusively — + # cycling overrides all additive logic. + if _cycled_weapon != null: + return [_cycled_weapon] + var base: Array[WeaponShot] = _compute_active_weapons() + if not base.is_empty(): + var additive: Array[WeaponShot] = base.filter( + func(w: WeaponShot): return not w.force_replace_all + ) + if not additive.is_empty(): + return additive + # If no non-force-replace weapons are active, return only the current + # default weapon (weapon_data). This ensures the starting weapon (stock) + # fires as a single shot rather than all force_replace_all weapons firing + # simultaneously. + if weapon_data != null and base.has(weapon_data): + return [weapon_data] + return base.filter( + func(w: WeaponShot): return w.force_replace_all + ) + + +func _get_force_replace_weapons() -> Array[WeaponShot]: + # Return weapons that override all others (force_replace_all = true). + return _compute_active_weapons().filter( + func(w: WeaponShot): return w.force_replace_all + ) + + +func collect_weapon(new_weapon: WeaponShot) -> void: + # Handle adding a weapon, applying replacement logic as needed. + # + # Replacement rules (applied in order): + # 1. force_replace_all: Discard all weapons, add only this one. + # Rule 2: replaces_at_same_level: Remove weapons at matching power level. + # 3. Normal: Add additive weapon (no removal). + # + # Sets the power level to this weapon's if higher than current. + # Emits weapon_changed with the new active set. + # + # Clear any active cycle — collecting a weapon during cycling restores + # normal additive behavior. + if _cycled_weapon != null: + reset_weapon_cycle() + # Rule 1: Force replace — discard everything, keep only this weapon + if new_weapon.force_replace_all: + available_weapons = [new_weapon] + _current_power_level = new_weapon.power_level + _update_data_from_active_weapons() + _emit_weapon_changed() + _trigger_change_flash() + return + + # Rule 2: Replace if same level — remove weapons at matching power level + if new_weapon.replaces_at_same_level: + var was_replaced: bool = false + for i in range(available_weapons.size()): + if available_weapons[i] != null and \ + available_weapons[i].replaces_at_same_level == false and \ + available_weapons[i].power_level == new_weapon.power_level: + available_weapons.remove_at(i) + was_replaced = true + break # Only replace the highest (first match at this level) + if was_replaced: + # Also remove any other weapons with the same power_level that may + # have sneaked in via previous power level upgrades + var filtered: Array[WeaponShot] = [] + for w: WeaponShot in available_weapons: + if w != null and w.power_level == new_weapon.power_level: + continue + filtered.append(w) + available_weapons = filtered + + # Rule 3: Additive — just add it (no removal) + var already_has: bool = false + for w: WeaponShot in available_weapons: + if w != null and w.shot_name == new_weapon.shot_name: + already_has = true + break + if not already_has: + available_weapons.append(new_weapon) + + # Update power level if this weapon is at a higher tier + if new_weapon.power_level > _current_power_level: + _current_power_level = new_weapon.power_level + + _update_data_from_active_weapons() + _emit_weapon_changed() + _trigger_change_flash() + + +func _update_data_from_active_weapons() -> void: + # Update weapon_data to point to the highest-priority active weapon. + # + # This is called after any weapon collection or power level change to + # keep the legacy `weapon_data` property in sync for backward compatibility. + # Force_replace_all weapons take priority over additive weapons. + # Check for force_replace weapons first (they override everything) + var force_replaces: Array[WeaponShot] = _get_force_replace_weapons() + if not force_replaces.is_empty(): + weapon_data = force_replaces[-1] + return + + # Otherwise, point to the highest-priority additive weapon + var active: Array[WeaponShot] = get_active_weapons() + if not active.is_empty(): + weapon_data = active[-1] + else: + weapon_data = null func _emit_weapon_changed() -> void: + if weapon_data == null: + return var ship: Sprite2D = get_parent().get_node("Ship") var sprite_size = ship.get_rect().size - var origin_offset: float = weapon_data.origin if weapon_data else 0.0 + var origin_offset: float = weapon_data.origin EventBus.weapon_changed.emit(weapon_data.shot_name, get_parent(), sprite_size, origin_offset) func get_bullet_scene() -> PackedScene: return weapon_data.bullet_scene if weapon_data else null + func get_weapon_resource() -> Resource: return weapon_data + func select_weapon_by_name(weapon_name: String) -> bool: for i in range(available_weapons.size()): - if available_weapons[i].shot_name == weapon_name: - _current_weapon_index = i - weapon_data = available_weapons[i] + var candidate: WeaponShot = available_weapons[i] + if candidate != null and candidate.shot_name == weapon_name: + _cycled_weapon = candidate # override additive behavior print("Switched to: ", weapon_name) - - _emit_weapon_changed() _trigger_change_flash() return true return false @@ -49,14 +192,36 @@ func _trigger_change_flash() -> void: effects_component.apply_effect(weapon_change_flash) - -func cycle_weapon() -> void: # Used for testing weapon cycling +func cycle_weapon() -> void: # Used for testing weapon cycling if available_weapons.is_empty(): return _current_weapon_index = (_current_weapon_index + 1) % available_weapons.size() - weapon_data = available_weapons[_current_weapon_index] - print("Switched to: ", weapon_data.shot_name) + var new_weapon: WeaponShot = available_weapons[_current_weapon_index] + if new_weapon == null: + return + print("Switched to: ", new_weapon.shot_name) + # Remove the previous weapon from the active set before engaging the next. + # Clearing _cycled_weapon forces get_active_weapons() to evaluate the + # additive state (which temporarily re-includes the old weapon). Then + # re-setting _cycled_weapon to the new weapon re-establishes the cycle + # override. This two-step process ensures the old weapon is fully removed + # from the active set and its power level doesn't leak into the new cycle. + _cycled_weapon = null # 1) Remove previous weapon + _cycled_weapon = new_weapon # 2) Engage next weapon + _current_power_level = new_weapon.power_level + + # Update weapon_data to reflect the cycled weapon directly, + # so _emit_weapon_changed() reports the correct name and origin. + weapon_data = new_weapon + _emit_weapon_changed() + _trigger_change_flash() + + +func reset_weapon_cycle() -> void: + # Restore normal additive behavior after cycling. + _cycled_weapon = null + _update_data_from_active_weapons() _emit_weapon_changed() _trigger_change_flash() diff --git a/scripts/weapon_shot.gd b/scripts/weapon_shot.gd index 8abc83d..a2f902a 100644 --- a/scripts/weapon_shot.gd +++ b/scripts/weapon_shot.gd @@ -82,3 +82,20 @@ extends Resource # Array of projectile groups defining layered, multi-directional fire patterns. @export var groups: Array[WeaponProjectileGroup] = [] + + +@export_subgroup("Power & Composition") + + +# The power level at which this weapon becomes active. +# Weapons with power_level <= current power level are considered active. +@export_range(0, 20, 1) var power_level: int = 0 + +# If true, collecting this weapon removes any existing weapon whose +# power_level matches this weapon's power_level. Use this instead of +# force_replace_all for rank-up replacements (e.g. stock → tri → spread). +@export var replaces_at_same_level: bool = false + +# If true, this weapon replaces ALL active weapons (ignores power_level). +# Used for rank-based transformations where the entire arsenal changes. +@export var force_replace_all: bool = false From 3ec8cb809552357b727029114c3259f370f9a273 Mon Sep 17 00:00:00 2001 From: henry Date: Tue, 30 Jun 2026 08:27:04 +0100 Subject: [PATCH 2/8] Added vanguard+ weapon icon. --- graphics/vanguard+.png | Bin 0 -> 272 bytes graphics/vanguard+.png.import | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 graphics/vanguard+.png create mode 100644 graphics/vanguard+.png.import diff --git a/graphics/vanguard+.png b/graphics/vanguard+.png new file mode 100644 index 0000000000000000000000000000000000000000..dee5a9786317dbc8951a20525944508e8d3eff3f GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1IggaDrqS0JsV zVB)G_lV)kyA7*{OQ0?Y|w0DPR{r+S&^JzfVJ_GY9dES}&LEb7xHkS7GB?bvi(RmM( zXaA2s`{eZPKmY&#pS^Z_OI>?rR#s$KP?=NL8Y82d;mOylclA&A>dH6xoxJBf&<=%? zAirP+plW27#MsK#aXLrj~)i Date: Tue, 30 Jun 2026 13:50:31 +0100 Subject: [PATCH 3/8] Set up power up spawner to test weapon collection functions. --- scenes/player.gd | 18 ++++++ scenes/player.tscn | 7 ++- scenes/power_up_spawner.tscn | 19 ++++++ scenes/world.tscn | 3 + scripts/event_bus.gd | 3 + scripts/power_up_spawner.gd | 105 ++++++++++++++++++++++++++++++++ scripts/power_up_spawner.gd.uid | 1 + scripts/weapon_component.gd | 2 +- 8 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 scenes/power_up_spawner.tscn create mode 100644 scripts/power_up_spawner.gd create mode 100644 scripts/power_up_spawner.gd.uid diff --git a/scenes/player.gd b/scenes/player.gd index ace3c7a..12ea90b 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -5,6 +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() # Get the viewport size for positioning @onready var screensize = get_viewport().content_scale_size @@ -55,3 +56,20 @@ func _process(delta) -> void: if input_component.cycle_weapon: weapon_component.cycle_weapon() $EffectsComponent.apply_effect(flash_effect) + +func _on_help_box_area_entered(area: Area2D) -> void: + # Check if the colliding area is a powerup pickup. + if not area.has_meta("weapon_shot"): + return + var weapon_shot: WeaponShot = area.get_meta("weapon_shot") + weapon_component.collect_weapon(weapon_shot) + powerup_spawner.pickup_collected() + area.queue_free() + + +func _find_powerup_spawner() -> PowerUpSpawner: + # The spawner is a sibling under Level — find it by searching parent. + var level: Node2D = get_parent() as Node2D + if level == null: + return null + return level.get_node_or_null("PowerupSpawner") as PowerUpSpawner diff --git a/scenes/player.tscn b/scenes/player.tscn index a39f299..4464b6d 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -128,7 +128,11 @@ shape = SubResource("RectangleShape2D_qlg0r") disabled = true debug_color = Color(1, 0.5058824, 0.21960784, 0.41960785) -[node name="HelpBox" type="CollisionShape2D" parent="." unique_id=938667427] +[node name="HelpBoxArea" type="Area2D" parent="." unique_id=938667427] +collision_layer = 2 +collision_mask = 1 + +[node name="HelpBoxShape" type="CollisionShape2D" parent="HelpBoxArea" unique_id=938667428] position = Vector2(0, 3) shape = SubResource("RectangleShape2D_tuyoq") debug_color = Color(0, 1, 0, 0.41960785) @@ -174,4 +178,5 @@ script = ExtResource("12_effcomp") target_node = NodePath("../Ship") metadata/_custom_type_script = "uid://65d3hbrpm21x" +[connection signal="area_entered" from="HelpBoxArea" to="." method="_on_help_box_area_entered"] [connection signal="timeout" from="WeaponCooldown" to="." method="_on_weapon_cooldown_timeout"] diff --git a/scenes/power_up_spawner.tscn b/scenes/power_up_spawner.tscn new file mode 100644 index 0000000..d099e50 --- /dev/null +++ b/scenes/power_up_spawner.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://du5nub2keaty3"] + +[ext_resource type="Script" uid="uid://qf6mgn4117o" path="res://scripts/power_up_spawner.gd" id="1_85wk8"] +[ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_07x44"] +[ext_resource type="Resource" uid="uid://b75ae840k03dy" path="res://resources/player_weapon_resources/stock.tres" id="3_0eev3"] +[ext_resource type="Resource" uid="uid://cck3gmnhu5agc" path="res://resources/player_weapon_resources/tri.tres" id="4_qqjui"] +[ext_resource type="Resource" uid="uid://ds53sryglmf27" path="res://resources/player_weapon_resources/spread.tres" id="5_mwa7c"] +[ext_resource type="Resource" uid="uid://bhc6aja38vyr" path="res://resources/player_weapon_resources/vanguard.tres" id="6_vphcr"] +[ext_resource type="Resource" uid="uid://c41aoyka7nfr8" path="res://resources/player_weapon_resources/vanguard+.tres" id="7_g67dp"] + +[node name="PowerUpSpawner" type="Node2D" unique_id=1191463626] +script = ExtResource("1_85wk8") +all_weapon_shots = Array[ExtResource("2_07x44")]([ExtResource("3_0eev3"), ExtResource("4_qqjui"), ExtResource("5_mwa7c"), ExtResource("6_vphcr"), ExtResource("7_g67dp")]) + +[node name="SpawnTimer" type="Timer" parent="." unique_id=173111015] +wait_time = 5.0 +autostart = true + +[connection signal="timeout" from="SpawnTimer" to="." method="_on_spawn_timer_timeout"] diff --git a/scenes/world.tscn b/scenes/world.tscn index 515828a..e415429 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -6,6 +6,7 @@ [ext_resource type="PackedScene" uid="uid://c3scm07fgpkcp" path="res://scenes/notification_display.tscn" id="3_71j4m"] [ext_resource type="PackedScene" uid="uid://6wq3ynesnsha" path="res://scenes/player.tscn" id="3_k0juu"] [ext_resource type="Script" uid="uid://eblemtukrey6" path="res://scripts/utility/fps_display.gd" id="4_qfnet"] +[ext_resource type="PackedScene" uid="uid://du5nub2keaty3" path="res://scenes/power_up_spawner.tscn" id="7_i7141"] [node name="World" type="Node2D" unique_id=1317852169] @@ -92,3 +93,5 @@ hframes = 4 frame = 3 [node name="Player" parent="Level" unique_id=652131079 instance=ExtResource("3_k0juu")] + +[node name="PowerupSpawner" parent="Level" unique_id=1191463626 instance=ExtResource("7_i7141")] diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index fc12a05..fa3dcc0 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -4,4 +4,7 @@ extends Node signal weapon_changed(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: float) +# Emitted when a powerup is collected by the player. +signal powerup_collected(weapon_shot: WeaponShot, player_node: Node2D) + @warning_ignore_restore("unused_signal") diff --git a/scripts/power_up_spawner.gd b/scripts/power_up_spawner.gd new file mode 100644 index 0000000..8185b71 --- /dev/null +++ b/scripts/power_up_spawner.gd @@ -0,0 +1,105 @@ +class_name PowerUpSpawner extends Node2D + +@export var spawn_interval: float = 5.0 +@export var test_mode: bool = true + +@onready var screensize: Vector2 = get_viewport().content_scale_size +@onready var spawn_timer: Timer = $SpawnTimer + +# All weapon resources to spawn as pickups. +# Populate in the editor with your weapon_shot resources. +@export var all_weapon_shots: Array[WeaponShot] = [] + +var _pickups_spawned: int = 0 +var _pickups_remaining: int = 0 + +func _ready() -> void: + if spawn_timer == null: + return + spawn_timer.wait_time = spawn_interval + + +func _on_spawn_timer_timeout() -> void: + if test_mode: + _spawn_test_pickups() + else: + # TODO: normal mode — spawn a single random pickup. + pass + + +func _spawn_test_pickups() -> void: + for weapon: WeaponShot in all_weapon_shots: + if weapon == null or weapon.shot_name.is_empty(): + continue + var pickup := _create_pickup(weapon) + if not is_instance_valid(pickup): + continue # skipped — no valid icon + + # Random position within viewport, with edge padding. + var margin: float = 15.0 + 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 + + # Pause the timer while pickups are on screen; resume when all gone. + if spawn_timer != null: + spawn_timer.stop() + + +func pickup_collected() -> void: + # Called by the player when a pickup is collected. + # 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 + + +func _create_pickup(weapon: WeaponShot) -> Area2D: + var pickup := Area2D.new() + pickup.name = "PowerUpPickup" + + # Icon texture — load from graphics/ matching the weapon's shot_name. + var icon_path := "res://graphics/" + weapon.shot_name.replace(" ", "") + ".png" + var icon_tex := load(icon_path) as Texture2D + if icon_tex == null: + printerr("[PowerUpSpawner] No texture found for weapon: ", weapon.shot_name) + pickup.queue_free() # return a dead node — caller skips it + return pickup + + var icon := TextureRect.new() + icon.texture = icon_tex + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.size = Vector2(16, 16) + pickup.add_child(icon) + + # Collision shape — small rectangle matching the icon. + var shape := RectangleShape2D.new() + shape.size = Vector2(14, 14) + var collision := CollisionShape2D.new() + collision.shape = shape + pickup.add_child(collision) + + # Store the weapon reference so we know what to give the player. + pickup.set_meta("weapon_shot", weapon) + + # Configure collision so HelpBoxArea (Layer 2, Mask 1) detects it. + pickup.collision_layer = 1 + pickup.collision_mask = 0 + + return pickup + + + + + +# --- Timer control --- + +# Direct stop/start — no guard conditions. The timer's paused property +# can cause deadlock if the guard checks "not paused" after stop(). diff --git a/scripts/power_up_spawner.gd.uid b/scripts/power_up_spawner.gd.uid new file mode 100644 index 0000000..7121649 --- /dev/null +++ b/scripts/power_up_spawner.gd.uid @@ -0,0 +1 @@ +uid://qf6mgn4117o diff --git a/scripts/weapon_component.gd b/scripts/weapon_component.gd index 63a916b..94ed851 100644 --- a/scripts/weapon_component.gd +++ b/scripts/weapon_component.gd @@ -8,7 +8,7 @@ var _current_weapon_index = 0 var _current_power_level = 0 var _cycled_weapon: WeaponShot = null -@onready var effects_component: EffectComponent = get_node("EffectsComponent") +@onready var effects_component: EffectComponent = get_node("../EffectsComponent") func _ready() -> void: # Set the default weapon to be stock, i.e. index 1 From da9f94778804661c9337d51d76abf9cd069e010d Mon Sep 17 00:00:00 2001 From: henry Date: Tue, 30 Jun 2026 20:16:26 +0100 Subject: [PATCH 4/8] Update spawner to be scene-based. --- resources/player_weapon_resources/spread.tres | 2 + resources/player_weapon_resources/stock.tres | 2 + resources/player_weapon_resources/tri.tres | 6 +- .../player_weapon_resources/vanguard+.tres | 2 + .../player_weapon_resources/vanguard.tres | 2 + scenes/power_up_spawner.tscn | 22 +++++--- scenes/powerup_pickup.tscn | 10 ++++ scripts/power_up_spawner.gd | 56 +++++++------------ scripts/weapon_shot.gd | 3 + 9 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 scenes/powerup_pickup.tscn diff --git a/resources/player_weapon_resources/spread.tres b/resources/player_weapon_resources/spread.tres index dc6b432..912569c 100644 --- a/resources/player_weapon_resources/spread.tres +++ b/resources/player_weapon_resources/spread.tres @@ -3,11 +3,13 @@ [ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_pn64g"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_4b7mw"] [ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_ak5x4"] +[ext_resource type="Texture2D" uid="uid://dwc5fjwhqikoo" path="res://graphics/spread.png" id="3_ak5x4"] [resource] script = ExtResource("2_4b7mw") shot_name = "spread" bullet_scene = ExtResource("1_pn64g") +pickup_icon = ExtResource("3_ak5x4") damage = 5 speed = 720 projectiles = 5 diff --git a/resources/player_weapon_resources/stock.tres b/resources/player_weapon_resources/stock.tres index 41c3bb3..f975f9a 100644 --- a/resources/player_weapon_resources/stock.tres +++ b/resources/player_weapon_resources/stock.tres @@ -3,11 +3,13 @@ [ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_lcw6o"] [ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_e245k"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_ym48c"] +[ext_resource type="Texture2D" uid="uid://dtggedl0127vk" path="res://graphics/stock.png" id="3_e245k"] [resource] script = ExtResource("2_ym48c") shot_name = "stock" bullet_scene = ExtResource("1_lcw6o") +pickup_icon = ExtResource("3_e245k") damage = 2 speed = 650 horizontal_offset = 12.0 diff --git a/resources/player_weapon_resources/tri.tres b/resources/player_weapon_resources/tri.tres index d13bcc6..be0811a 100644 --- a/resources/player_weapon_resources/tri.tres +++ b/resources/player_weapon_resources/tri.tres @@ -1,13 +1,15 @@ [gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://cck3gmnhu5agc"] -[ext_resource type="PackedScene" uid="uid://clpyd8qfwthk2" path="res://scenes/player_weapons/weapon_spreadshot.tscn" id="1_07hyh"] +[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_bhv8k"] [ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_bhv8k"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_syw6j"] +[ext_resource type="Texture2D" uid="uid://cne450xyao5xl" path="res://graphics/tri.png" id="3_fw2v3"] [resource] script = ExtResource("2_syw6j") shot_name = "tri" -bullet_scene = ExtResource("1_07hyh") +bullet_scene = ExtResource("1_bhv8k") +pickup_icon = ExtResource("3_fw2v3") damage = 3 speed = 700 projectiles = 3 diff --git a/resources/player_weapon_resources/vanguard+.tres b/resources/player_weapon_resources/vanguard+.tres index b8b46ab..6397f26 100644 --- a/resources/player_weapon_resources/vanguard+.tres +++ b/resources/player_weapon_resources/vanguard+.tres @@ -4,11 +4,13 @@ [ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_qeb4b"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="3_fh2eq"] [ext_resource type="Resource" uid="uid://ch8qch6lx1mdx" path="res://resources/player_weapon_resources/patterns/phallanx.tres" id="3_qeb4b"] +[ext_resource type="Texture2D" uid="uid://dsjhp3mh5pqg3" path="res://graphics/vanguard+.png" id="4_fh2eq"] [resource] script = ExtResource("3_fh2eq") shot_name = "vanguard+" bullet_scene = ExtResource("1_lqlv8") +pickup_icon = ExtResource("4_fh2eq") damage = 5 speed = 500 projectiles = 5 diff --git a/resources/player_weapon_resources/vanguard.tres b/resources/player_weapon_resources/vanguard.tres index 0970935..3e6a379 100644 --- a/resources/player_weapon_resources/vanguard.tres +++ b/resources/player_weapon_resources/vanguard.tres @@ -3,11 +3,13 @@ [ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_hm46f"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_w7xlm"] [ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_xf183"] +[ext_resource type="Texture2D" uid="uid://b34vaaw3u84ae" path="res://graphics/vanguard.png" id="3_xf183"] [resource] script = ExtResource("2_w7xlm") shot_name = "vanguard" bullet_scene = ExtResource("1_hm46f") +pickup_icon = ExtResource("3_xf183") damage = 5 speed = 500 projectiles = 5 diff --git a/scenes/power_up_spawner.tscn b/scenes/power_up_spawner.tscn index d099e50..ac4dce4 100644 --- a/scenes/power_up_spawner.tscn +++ b/scenes/power_up_spawner.tscn @@ -1,16 +1,20 @@ [gd_scene format=3 uid="uid://du5nub2keaty3"] -[ext_resource type="Script" uid="uid://qf6mgn4117o" path="res://scripts/power_up_spawner.gd" id="1_85wk8"] -[ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="2_07x44"] -[ext_resource type="Resource" uid="uid://b75ae840k03dy" path="res://resources/player_weapon_resources/stock.tres" id="3_0eev3"] -[ext_resource type="Resource" uid="uid://cck3gmnhu5agc" path="res://resources/player_weapon_resources/tri.tres" id="4_qqjui"] -[ext_resource type="Resource" uid="uid://ds53sryglmf27" path="res://resources/player_weapon_resources/spread.tres" id="5_mwa7c"] -[ext_resource type="Resource" uid="uid://bhc6aja38vyr" path="res://resources/player_weapon_resources/vanguard.tres" id="6_vphcr"] -[ext_resource type="Resource" uid="uid://c41aoyka7nfr8" path="res://resources/player_weapon_resources/vanguard+.tres" id="7_g67dp"] +[ext_resource type="PackedScene" uid="uid://c9k2p5m8xrq4" path="res://scenes/powerup_pickup.tscn" id="1_xk8p2"] +[ext_resource type="Script" uid="uid://qf6mgn4117o" path="res://scripts/power_up_spawner.gd" id="2_9m3n7"] +[ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="3_07x44"] +[ext_resource type="Resource" uid="uid://b75ae840k03dy" path="res://resources/player_weapon_resources/stock.tres" id="4_0eev3"] +[ext_resource type="Resource" uid="uid://cck3gmnhu5agc" path="res://resources/player_weapon_resources/tri.tres" id="5_qqjui"] +[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"] [node name="PowerUpSpawner" type="Node2D" unique_id=1191463626] -script = ExtResource("1_85wk8") -all_weapon_shots = Array[ExtResource("2_07x44")]([ExtResource("3_0eev3"), ExtResource("4_qqjui"), ExtResource("5_mwa7c"), ExtResource("6_vphcr"), ExtResource("7_g67dp")]) +script = ExtResource("2_9m3n7") +pickup_scene = ExtResource("1_xk8p2") +all_weapon_shots = Array[ExtResource("3_07x44")]( + [ExtResource("4_0eev3"), ExtResource("5_qqjui"), ExtResource("6_mwa7c"), ExtResource("7_vphcr"), ExtResource("8_g67dp")] +) [node name="SpawnTimer" type="Timer" parent="." unique_id=173111015] wait_time = 5.0 diff --git a/scenes/powerup_pickup.tscn b/scenes/powerup_pickup.tscn new file mode 100644 index 0000000..8073dac --- /dev/null +++ b/scenes/powerup_pickup.tscn @@ -0,0 +1,10 @@ +[gd_scene format=3 uid="uid://dsqfuv86suu17"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_mmgg1"] +size = Vector2(14, 14) + +[node name="PowerUpPickup" type="Area2D" unique_id=478708932] +collision_mask = 0 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=347643413] +shape = SubResource("RectangleShape2D_mmgg1") diff --git a/scripts/power_up_spawner.gd b/scripts/power_up_spawner.gd index 8185b71..8387f34 100644 --- a/scripts/power_up_spawner.gd +++ b/scripts/power_up_spawner.gd @@ -3,6 +3,9 @@ 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 @@ -31,7 +34,8 @@ func _spawn_test_pickups() -> void: for weapon: WeaponShot in all_weapon_shots: if weapon == null or weapon.shot_name.is_empty(): continue - var pickup := _create_pickup(weapon) + + var pickup := _spawn_pickup(weapon) if not is_instance_valid(pickup): continue # skipped — no valid icon @@ -61,45 +65,27 @@ func pickup_collected() -> void: _pickups_spawned = 0 -func _create_pickup(weapon: WeaponShot) -> Area2D: - var pickup := Area2D.new() - pickup.name = "PowerUpPickup" +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 - # Icon texture — load from graphics/ matching the weapon's shot_name. - var icon_path := "res://graphics/" + weapon.shot_name.replace(" ", "") + ".png" - var icon_tex := load(icon_path) as Texture2D - if icon_tex == null: - printerr("[PowerUpSpawner] No texture found for weapon: ", weapon.shot_name) - pickup.queue_free() # return a dead node — caller skips it - return pickup + # 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 = icon_tex + icon.texture = weapon.pickup_icon icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.size = Vector2(16, 16) pickup.add_child(icon) - # Collision shape — small rectangle matching the icon. - var shape := RectangleShape2D.new() - shape.size = Vector2(14, 14) - var collision := CollisionShape2D.new() - collision.shape = shape - pickup.add_child(collision) - - # Store the weapon reference so we know what to give the player. - pickup.set_meta("weapon_shot", weapon) - - # Configure collision so HelpBoxArea (Layer 2, Mask 1) detects it. - pickup.collision_layer = 1 - pickup.collision_mask = 0 - return pickup - - - - - -# --- Timer control --- - -# Direct stop/start — no guard conditions. The timer's paused property -# can cause deadlock if the guard checks "not paused" after stop(). diff --git a/scripts/weapon_shot.gd b/scripts/weapon_shot.gd index a2f902a..fb6ecdc 100644 --- a/scripts/weapon_shot.gd +++ b/scripts/weapon_shot.gd @@ -11,6 +11,9 @@ extends Resource # PackedScene reference to the bullet / projectile scene instantiated on fire. @export var bullet_scene: PackedScene +# Icon used when this weapon appears as a powerup pickup on the field. +@export var pickup_icon: Texture2D + @export_subgroup("Launch") From 49a8913c14b4e20311447d7a722f1556cf093571 Mon Sep 17 00:00:00 2001 From: henry Date: Wed, 1 Jul 2026 15:03:12 +0100 Subject: [PATCH 5/8] Updated future testing logic. --- scenes/player.tscn | 2 +- scripts/power_up_spawner.gd | 7 +++++++ scripts/weapon_component.gd | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scenes/player.tscn b/scenes/player.tscn index 4464b6d..165f5b3 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -130,7 +130,6 @@ debug_color = Color(1, 0.5058824, 0.21960784, 0.41960785) [node name="HelpBoxArea" type="Area2D" parent="." unique_id=938667427] collision_layer = 2 -collision_mask = 1 [node name="HelpBoxShape" type="CollisionShape2D" parent="HelpBoxArea" unique_id=938667428] position = Vector2(0, 3) @@ -170,6 +169,7 @@ script = ExtResource("7_d2wvv") weapon_data = ExtResource("8_stock") available_weapons = Array[ExtResource("10_d2wvv")]([ExtResource("8_stock"), ExtResource("11_3v2ag"), ExtResource("13_f1ej7"), ExtResource("9_ur7pv"), ExtResource("14_oprun")]) weapon_change_flash = SubResource("Resource_jej6c") +test_mode = true metadata/_custom_type_script = "uid://ylmao2ndp22y" [node name="EffectsComponent" type="Node" parent="." unique_id=1393438576 node_paths=PackedStringArray("target_node")] diff --git a/scripts/power_up_spawner.gd b/scripts/power_up_spawner.gd index 8387f34..bfabd6d 100644 --- a/scripts/power_up_spawner.gd +++ b/scripts/power_up_spawner.gd @@ -57,6 +57,13 @@ func _spawn_test_pickups() -> void: 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 # Decrement counter; resume timer once all pickups are gone. _pickups_remaining = max(0, _pickups_remaining - 1) if _pickups_remaining <= 0: diff --git a/scripts/weapon_component.gd b/scripts/weapon_component.gd index 94ed851..90083b0 100644 --- a/scripts/weapon_component.gd +++ b/scripts/weapon_component.gd @@ -4,6 +4,8 @@ class_name WeaponComponent extends Node @export var available_weapons: Array[WeaponShot] = [] @export var weapon_change_flash: BaseEffect = null +@export var test_mode: bool = false + var _current_weapon_index = 0 var _current_power_level = 0 var _cycled_weapon: WeaponShot = null @@ -90,7 +92,7 @@ func collect_weapon(new_weapon: WeaponShot) -> void: # # Clear any active cycle — collecting a weapon during cycling restores # normal additive behavior. - if _cycled_weapon != null: + if _cycled_weapon != null and not test_mode: reset_weapon_cycle() # Rule 1: Force replace — discard everything, keep only this weapon if new_weapon.force_replace_all: From a0df0be86213d77e041c894963f154c54b04b8c5 Mon Sep 17 00:00:00 2001 From: henry Date: Thu, 2 Jul 2026 08:24:02 +0100 Subject: [PATCH 6/8] Minor testing logic update. --- scripts/power_up_spawner.gd | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/power_up_spawner.gd b/scripts/power_up_spawner.gd index bfabd6d..716eecf 100644 --- a/scripts/power_up_spawner.gd +++ b/scripts/power_up_spawner.gd @@ -26,8 +26,36 @@ func _on_spawn_timer_timeout() -> void: if test_mode: _spawn_test_pickups() else: - # TODO: normal mode — spawn a single random pickup. - pass + _spawn_normal_pickup() + + +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: From 935b61bd3b18256bfdc66f8c2d9655412633c23a Mon Sep 17 00:00:00 2001 From: henry Date: Thu, 2 Jul 2026 15:44:23 +0100 Subject: [PATCH 7/8] Set up framework for more advance pickup behaviours. --- resources/pickups/drift_diagonal.tres | 14 ++ resources/pickups/drift_leaf_fall.tres | 14 ++ resources/pickups/drift_sine_wave.tres | 13 ++ resources/pickups/pickup_behavior.gd | 43 ++++ resources/pickups/pickup_behavior.gd.uid | 1 + resources/pickups/pickup_drift_behavior.gd | 47 +++++ .../pickups/pickup_drift_behavior.gd.uid | 1 + resources/pickups/rare_powerup.tres | 13 ++ resources/pickups/std_powerup.tres | 8 + scenes/player.gd | 5 +- scenes/player.tscn | 3 + scenes/power_up_spawner.tscn | 11 + scenes/powerup_pickup.tscn | 7 + scripts/pickups/pickup_behavior_controller.gd | 132 ++++++++++++ .../pickups/pickup_behavior_controller.gd.uid | 1 + scripts/power_up_spawner.gd | 192 ++++++++++++++++-- 16 files changed, 485 insertions(+), 20 deletions(-) create mode 100644 resources/pickups/drift_diagonal.tres create mode 100644 resources/pickups/drift_leaf_fall.tres create mode 100644 resources/pickups/drift_sine_wave.tres create mode 100644 resources/pickups/pickup_behavior.gd create mode 100644 resources/pickups/pickup_behavior.gd.uid create mode 100644 resources/pickups/pickup_drift_behavior.gd create mode 100644 resources/pickups/pickup_drift_behavior.gd.uid create mode 100644 resources/pickups/rare_powerup.tres create mode 100644 resources/pickups/std_powerup.tres create mode 100644 scripts/pickups/pickup_behavior_controller.gd create mode 100644 scripts/pickups/pickup_behavior_controller.gd.uid 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 From 92463f4465eea1015170eaa49015f798adc9693b Mon Sep 17 00:00:00 2001 From: henry Date: Mon, 6 Jul 2026 14:10:14 +0100 Subject: [PATCH 8/8] Cleaned up a few inconsistencies in pickup behaviours. --- resources/pickups/pickup_behavior.gd | 3 --- resources/pickups/pickup_drift_behavior.gd | 7 +++--- scripts/pickups/pickup_behavior_controller.gd | 23 +++++++++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/resources/pickups/pickup_behavior.gd b/resources/pickups/pickup_behavior.gd index 6dcc2e7..2387151 100644 --- a/resources/pickups/pickup_behavior.gd +++ b/resources/pickups/pickup_behavior.gd @@ -20,9 +20,6 @@ class_name PickupBehavior extends Resource ## 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 diff --git a/resources/pickups/pickup_drift_behavior.gd b/resources/pickups/pickup_drift_behavior.gd index 11c6f4f..24f531b 100644 --- a/resources/pickups/pickup_drift_behavior.gd +++ b/resources/pickups/pickup_drift_behavior.gd @@ -40,8 +40,9 @@ enum DriftType { ## Phase offset in radians — randomize per-instance for variety. @export var phase_offset: float = 0.0 -var _initial_x: float = 0.0 +var _elapsed: float = 0.0 -func reset(initial_position: Vector2) -> void: - _initial_x = initial_position.x +func start() -> void: + ## Reset drift state when a new pickup is spawned. + _elapsed = 0.0 diff --git a/scripts/pickups/pickup_behavior_controller.gd b/scripts/pickups/pickup_behavior_controller.gd index 783e784..84a8a4f 100644 --- a/scripts/pickups/pickup_behavior_controller.gd +++ b/scripts/pickups/pickup_behavior_controller.gd @@ -22,13 +22,23 @@ func _ready() -> void: if behavior != null: behavior.start() + if drift_behavior != null: + drift_behavior.start() func _process(delta: float) -> void: - if behavior == null or drift_behavior == null: + # Handle partial configurations: if only behavior is set, compute bob/shake. + # If only drift_behavior is set, compute drift offset. If both are set, + # composite them (bob/shake on icon, drift on pickup position). + var has_behavior: bool = behavior != null + var has_drift: bool = drift_behavior != null + + if not has_behavior and not has_drift: return - var alpha: float = behavior.process(delta) + var alpha: float = 1.0 + if has_behavior: + alpha = behavior.process(delta) if _icon_node != null: _icon_node.visible = alpha > 0.01 @@ -127,6 +137,9 @@ func _compute_drift_offset() -> Vector2: func _elapsed_time() -> float: - if behavior == null: - return 0.0 - return behavior._elapsed + # Prefer behavior's elapsed time; fall back to drift_behavior's. + if behavior != null: + return behavior._elapsed + if drift_behavior != null: + return drift_behavior._elapsed + return 0.0