Compare commits

..

No commits in common. "pickups" and "main" have entirely different histories.

30 changed files with 34 additions and 960 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 B

View file

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dsjhp3mh5pqg3"
path="res://.godot/imported/vanguard+.png-f0cc118ae237fbac7d80b37a3b3de33e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://graphics/vanguard+.png"
dest_files=["res://.godot/imported/vanguard+.png-f0cc118ae237fbac7d80b37a3b3de33e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,14 +0,0 @@
[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

View file

@ -1,14 +0,0 @@
[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

View file

@ -1,13 +0,0 @@
[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

View file

@ -1,40 +0,0 @@
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
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

View file

@ -1 +0,0 @@
uid://d0l8e2ymk0lx1

View file

@ -1,48 +0,0 @@
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 _elapsed: float = 0.0
func start() -> void:
## Reset drift state when a new pickup is spawned.
_elapsed = 0.0

View file

@ -1 +0,0 @@
uid://b4lar4f3526b1

View file

@ -1,13 +0,0 @@
[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

View file

@ -1,8 +0,0 @@
[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

View file

@ -3,13 +3,11 @@
[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_pn64g"] [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://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="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] [resource]
script = ExtResource("2_4b7mw") script = ExtResource("2_4b7mw")
shot_name = "spread" shot_name = "spread"
bullet_scene = ExtResource("1_pn64g") bullet_scene = ExtResource("1_pn64g")
pickup_icon = ExtResource("3_ak5x4")
damage = 5 damage = 5
speed = 720 speed = 720
projectiles = 5 projectiles = 5
@ -17,6 +15,4 @@ horizontal_offset = 8.25
spread_angle = 25.0 spread_angle = 25.0
stagger_offset = 0.5 stagger_offset = 0.5
stagger_animation = true stagger_animation = true
power_level = 1
force_replace_all = true
metadata/_custom_type_script = "uid://7n1itonn35fm" metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -3,17 +3,13 @@
[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_lcw6o"] [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://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="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] [resource]
script = ExtResource("2_ym48c") script = ExtResource("2_ym48c")
shot_name = "stock" shot_name = "stock"
bullet_scene = ExtResource("1_lcw6o") bullet_scene = ExtResource("1_lcw6o")
pickup_icon = ExtResource("3_e245k")
damage = 2 damage = 2
speed = 650 speed = 650
horizontal_offset = 12.0 horizontal_offset = 12.0
stagger_offset = 0.0 stagger_offset = 0.0
power_level = 1
force_replace_all = true
metadata/_custom_type_script = "uid://7n1itonn35fm" metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -1,21 +1,17 @@
[gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://cck3gmnhu5agc"] [gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://cck3gmnhu5agc"]
[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_bhv8k"] [ext_resource type="PackedScene" uid="uid://clpyd8qfwthk2" path="res://scenes/player_weapons/weapon_spreadshot.tscn" id="1_07hyh"]
[ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_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="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] [resource]
script = ExtResource("2_syw6j") script = ExtResource("2_syw6j")
shot_name = "tri" shot_name = "tri"
bullet_scene = ExtResource("1_bhv8k") bullet_scene = ExtResource("1_07hyh")
pickup_icon = ExtResource("3_fw2v3")
damage = 3 damage = 3
speed = 700 speed = 700
projectiles = 3 projectiles = 3
horizontal_offset = 12.5 horizontal_offset = 12.5
stagger_offset = 0.35 stagger_offset = 0.35
stagger_animation = true stagger_animation = true
power_level = 1
force_replace_all = true
metadata/_custom_type_script = "uid://7n1itonn35fm" metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -4,13 +4,11 @@
[ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="2_qeb4b"] [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="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="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] [resource]
script = ExtResource("3_fh2eq") script = ExtResource("3_fh2eq")
shot_name = "vanguard+" shot_name = "vanguard+"
bullet_scene = ExtResource("1_lqlv8") bullet_scene = ExtResource("1_lqlv8")
pickup_icon = ExtResource("4_fh2eq")
damage = 5 damage = 5
speed = 500 speed = 500
projectiles = 5 projectiles = 5
@ -20,6 +18,4 @@ compensate_spawn_geometry = true
stagger_animation = true stagger_animation = true
follow_angle = true follow_angle = true
groups = Array[ExtResource("2_qeb4b")]([ExtResource("3_qeb4b")]) groups = Array[ExtResource("2_qeb4b")]([ExtResource("3_qeb4b")])
power_level = 1
force_replace_all = true
metadata/_custom_type_script = "uid://7n1itonn35fm" metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -3,19 +3,15 @@
[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_hm46f"] [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://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="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] [resource]
script = ExtResource("2_w7xlm") script = ExtResource("2_w7xlm")
shot_name = "vanguard" shot_name = "vanguard"
bullet_scene = ExtResource("1_hm46f") bullet_scene = ExtResource("1_hm46f")
pickup_icon = ExtResource("3_xf183")
damage = 5 damage = 5
speed = 500 speed = 500
projectiles = 5 projectiles = 5
horizontal_offset = 8.25 horizontal_offset = 8.25
stagger_offset = 0.5 stagger_offset = 0.5
stagger_animation = true stagger_animation = true
power_level = 1
force_replace_all = true
metadata/_custom_type_script = "uid://7n1itonn35fm" metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -5,7 +5,6 @@ class_name Player extends Area2D
@onready var movement_component: MovementComponent = %MovementComponent @onready var movement_component: MovementComponent = %MovementComponent
@onready var shoot_component: ShootComponent = %ShootComponent @onready var shoot_component: ShootComponent = %ShootComponent
@onready var weapon_component: WeaponComponent = %WeaponComponent @onready var weapon_component: WeaponComponent = %WeaponComponent
@onready var powerup_spawner: PowerUpSpawner = _find_powerup_spawner()
# Get the viewport size for positioning # Get the viewport size for positioning
@onready var screensize = get_viewport().content_scale_size @onready var screensize = get_viewport().content_scale_size
@ -56,21 +55,3 @@ func _process(delta) -> void:
if input_component.cycle_weapon: if input_component.cycle_weapon:
weapon_component.cycle_weapon() weapon_component.cycle_weapon()
$EffectsComponent.apply_effect(flash_effect) $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)
if powerup_spawner != null:
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

View file

@ -10,12 +10,12 @@
[ext_resource type="Script" uid="uid://ylmao2ndp22y" path="res://scripts/weapon_component.gd" id="7_d2wvv"] [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://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="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="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="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="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://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="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"] [sub_resource type="AtlasTexture" id="AtlasTexture_tuyoq"]
atlas = ExtResource("5_qlg0r") atlas = ExtResource("5_qlg0r")
@ -100,9 +100,6 @@ size = Vector2(6, 5.75)
[node name="Player" type="Area2D" unique_id=652131079] [node name="Player" type="Area2D" unique_id=652131079]
script = ExtResource("1_ur7pv") script = ExtResource("1_ur7pv")
can_shoot = null
is_shooting = null
muzzle_flash = null
[node name="Ship" type="Sprite2D" parent="." unique_id=1155866924] [node name="Ship" type="Sprite2D" parent="." unique_id=1155866924]
unique_name_in_owner = true unique_name_in_owner = true
@ -131,15 +128,12 @@ shape = SubResource("RectangleShape2D_qlg0r")
disabled = true disabled = true
debug_color = Color(1, 0.5058824, 0.21960784, 0.41960785) debug_color = Color(1, 0.5058824, 0.21960784, 0.41960785)
[node name="HelpBoxArea" type="Area2D" parent="." unique_id=938667427] [node name="HelpBox" type="CollisionShape2D" parent="." unique_id=938667427]
collision_layer = 2
[node name="HelpBoxShape" type="CollisionShape2D" parent="HelpBoxArea" unique_id=938667428]
position = Vector2(0, 3) position = Vector2(0, 3)
shape = SubResource("RectangleShape2D_tuyoq") shape = SubResource("RectangleShape2D_tuyoq")
debug_color = Color(0, 1, 0, 0.41960785) debug_color = Color(0, 1, 0, 0.41960785)
[node name="HitBox" type="CollisionShape2D" parent="." unique_id=484826453] [node name="HitBox" type="CollisionShape2D" parent="." unique_id=485826453]
position = Vector2(0, 2) position = Vector2(0, 2)
shape = SubResource("RectangleShape2D_dqkch") shape = SubResource("RectangleShape2D_dqkch")
debug_color = Color(0.9843137, 0, 0, 0.80784315) debug_color = Color(0.9843137, 0, 0, 0.80784315)
@ -170,9 +164,8 @@ script = ExtResource("6_y4r1p")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("7_d2wvv") script = ExtResource("7_d2wvv")
weapon_data = ExtResource("8_stock") 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")]) available_weapons = Array[ExtResource("10_d2wvv")]([ExtResource("8_stock"), ExtResource("11_3v2ag"), ExtResource("9_ur7pv"), ExtResource("13_f1ej7"), ExtResource("14_oprun")])
weapon_change_flash = SubResource("Resource_jej6c") weapon_change_flash = SubResource("Resource_jej6c")
test_mode = true
metadata/_custom_type_script = "uid://ylmao2ndp22y" metadata/_custom_type_script = "uid://ylmao2ndp22y"
[node name="EffectsComponent" type="Node" parent="." unique_id=1393438576 node_paths=PackedStringArray("target_node")] [node name="EffectsComponent" type="Node" parent="." unique_id=1393438576 node_paths=PackedStringArray("target_node")]
@ -181,5 +174,4 @@ script = ExtResource("12_effcomp")
target_node = NodePath("../Ship") target_node = NodePath("../Ship")
metadata/_custom_type_script = "uid://65d3hbrpm21x" 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"] [connection signal="timeout" from="WeaponCooldown" to="." method="_on_weapon_cooldown_timeout"]

View file

@ -1,34 +0,0 @@
[gd_scene format=3 uid="uid://du5nub2keaty3"]
[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"]
[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")]
)
[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"]

View file

@ -1,17 +0,0 @@
[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]
shape = SubResource("RectangleShape2D_mmgg1")

View file

@ -6,7 +6,6 @@
[ext_resource type="PackedScene" uid="uid://c3scm07fgpkcp" path="res://scenes/notification_display.tscn" id="3_71j4m"] [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="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="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] [node name="World" type="Node2D" unique_id=1317852169]
@ -93,5 +92,3 @@ hframes = 4
frame = 3 frame = 3
[node name="Player" parent="Level" unique_id=652131079 instance=ExtResource("3_k0juu")] [node name="Player" parent="Level" unique_id=652131079 instance=ExtResource("3_k0juu")]
[node name="PowerupSpawner" parent="Level" unique_id=1191463626 instance=ExtResource("7_i7141")]

View file

@ -4,7 +4,4 @@ extends Node
signal weapon_changed(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: float) 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") @warning_ignore_restore("unused_signal")

View file

@ -33,9 +33,6 @@ func show_notification(weapon_name: String, player_node: Node2D, player_sprite_s
var icon_path = "res://graphics/" + weapon_name.to_lower() + ".png" var icon_path = "res://graphics/" + weapon_name.to_lower() + ".png"
if ResourceLoader.exists(icon_path): if ResourceLoader.exists(icon_path):
icon.texture = load(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: else:
icon.texture = null icon.texture = null

View file

@ -1,145 +0,0 @@
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()
if drift_behavior != null:
drift_behavior.start()
func _process(delta: float) -> void:
# 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 = 1.0
if has_behavior:
alpha = 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:
# 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

View file

@ -1 +0,0 @@
uid://c4a7husvaq2eo

View file

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

View file

@ -1 +0,0 @@
uid://qf6mgn4117o

View file

@ -5,24 +5,13 @@ class_name ShootComponent extends Node
@onready var ship: Sprite2D = get_node("../Ship") @onready var ship: Sprite2D = get_node("../Ship")
@onready var fire_helper: WeaponFireHelper = %WeaponFireHelper @onready var fire_helper: WeaponFireHelper = %WeaponFireHelper
func shoot() -> void: func shoot():
var active_weapons: Array[WeaponShot] = weapon_component.get_active_weapons() var weapon_data = weapon_component.weapon_data
if active_weapons.is_empty(): var current_time = Time.get_ticks_msec() / 1000.0
return
var current_time: float = Time.get_ticks_msec() / 1000.0 if player.travel > weapon_data.projectile_vertical_spacing:
_shoot_standard(weapon_data, current_time)
# Burst gating: find the minimum projectile_vertical_spacing among all _shoot_grouped(weapon_data, current_time)
# 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 # Reset travel after firing to prevent multiple bursts per tap
player.travel = 0.0 player.travel = 0.0
@ -37,12 +26,12 @@ func _get_spawn_position(weapon_data: WeaponShot) -> Vector2:
sprite_height = ship.texture.get_height() sprite_height = ship.texture.get_height()
# Top edge of the sprite in world space, then offset by origin. # Top edge of the sprite in world space, then offset by origin.
var top_edge: float = ship.global_position.y - (sprite_height / 2.0) var top_edge = ship.global_position.y - (sprite_height / 2.0)
return Vector2(ship.global_position.x, top_edge + weapon_data.origin) return Vector2(ship.global_position.x, top_edge + weapon_data.origin)
# Standard projectile logic (delegates to WeaponFireHelper) # Standard projectile logic (delegates to WeaponFireHelper)
func _shoot_standard(weapon_data: WeaponShot, current_time: float) -> void: func _shoot_standard(weapon_data: WeaponShot, current_time: float):
var total_projectiles: int = weapon_data.projectiles var total_projectiles: int = weapon_data.projectiles
if total_projectiles > 0: if total_projectiles > 0:
@ -51,8 +40,8 @@ func _shoot_standard(weapon_data: WeaponShot, current_time: float) -> void:
get_tree().root.add_child(bullet) get_tree().root.add_child(bullet)
# Delegate math to the helper # Delegate math to the helper
var spawn_position: Vector2 = _get_spawn_position(weapon_data) var spawn_position = _get_spawn_position(weapon_data)
var props: Dictionary = fire_helper.compute_standard_props(weapon_data, b, spawn_position, current_time) var props = fire_helper.compute_standard_props(weapon_data, b, spawn_position, current_time)
bullet.position = props.position bullet.position = props.position
bullet.angle = props.angle bullet.angle = props.angle
bullet.time_offset = props.time_offset bullet.time_offset = props.time_offset
@ -67,15 +56,15 @@ func _shoot_standard(weapon_data: WeaponShot, current_time: float) -> void:
# Grouped projectile logic (delegates to WeaponFireHelper) # Grouped projectile logic (delegates to WeaponFireHelper)
func _shoot_grouped(weapon_data: WeaponShot, current_time: float) -> void: func _shoot_grouped(weapon_data: WeaponShot, current_time: float):
for group: WeaponProjectileGroup in weapon_data.groups.filter(func(g): return g != null): for group: WeaponProjectileGroup in weapon_data.groups.filter(func(g): return g != null):
for b in range(group.bullet_count): for b in range(group.bullet_count):
var bullet := weapon_data.bullet_scene.instantiate() as Area2D var bullet := weapon_data.bullet_scene.instantiate() as Area2D
get_tree().root.add_child(bullet) get_tree().root.add_child(bullet)
# Apply computed properties for the original bullet # Apply computed properties for the original bullet
var spawn_position: Vector2 = _get_spawn_position(weapon_data) var spawn_position = _get_spawn_position(weapon_data)
var props: Dictionary = fire_helper.compute_grouped_props(group, weapon_data, b, spawn_position, current_time, false) var props = fire_helper.compute_grouped_props(group, weapon_data, b, spawn_position, current_time, false)
bullet.position = props.position bullet.position = props.position
bullet.angle = props.angle bullet.angle = props.angle
bullet.time_offset = props.time_offset bullet.time_offset = props.time_offset
@ -90,8 +79,8 @@ func _shoot_grouped(weapon_data: WeaponShot, current_time: float) -> void:
get_tree().root.add_child(mirror_bullet) get_tree().root.add_child(mirror_bullet)
# Apply computed properties for the mirrored bullet # Apply computed properties for the mirrored bullet
var mirror_spawn: Vector2 = _get_spawn_position(weapon_data) var mirror_spawn = _get_spawn_position(weapon_data)
var mirror_props: Dictionary = fire_helper.compute_grouped_props(group, weapon_data, b, mirror_spawn, current_time, true) var mirror_props = fire_helper.compute_grouped_props(group, weapon_data, b, mirror_spawn, current_time, true)
mirror_bullet.position = mirror_props.position mirror_bullet.position = mirror_props.position
mirror_bullet.angle = mirror_props.angle mirror_bullet.angle = mirror_props.angle
mirror_bullet.time_offset = mirror_props.time_offset mirror_bullet.time_offset = mirror_props.time_offset

View file

@ -4,186 +4,41 @@ class_name WeaponComponent extends Node
@export var available_weapons: Array[WeaponShot] = [] @export var available_weapons: Array[WeaponShot] = []
@export var weapon_change_flash: BaseEffect = null @export var weapon_change_flash: BaseEffect = null
@export var test_mode: bool = false var _current_weapon_index: int = 0
var _current_weapon_index = 0 @onready var effects_component: EffectComponent = %EffectsComponent
var _current_power_level = 0
var _cycled_weapon: WeaponShot = null
@onready var effects_component: EffectComponent = get_node("../EffectsComponent")
func _ready() -> void: func _ready() -> void:
# Set the default weapon to be stock, i.e. index 1 # Set the default weapon to be stock, i.e. index 1
if weapon_data and not available_weapons.is_empty(): if weapon_data and not available_weapons.is_empty():
for i in range(available_weapons.size()): for i in range(available_weapons.size()):
if available_weapons[i] == weapon_data: if available_weapons[i] == weapon_data:
_current_weapon_index = i _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 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 and not test_mode:
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: func _emit_weapon_changed() -> void:
if weapon_data == null:
return
var ship: Sprite2D = get_parent().get_node("Ship") var ship: Sprite2D = get_parent().get_node("Ship")
var sprite_size = ship.get_rect().size var sprite_size = ship.get_rect().size
var origin_offset: float = weapon_data.origin var origin_offset: float = weapon_data.origin if weapon_data else 0.0
EventBus.weapon_changed.emit(weapon_data.shot_name, get_parent(), sprite_size, origin_offset) EventBus.weapon_changed.emit(weapon_data.shot_name, get_parent(), sprite_size, origin_offset)
func get_bullet_scene() -> PackedScene: func get_bullet_scene() -> PackedScene:
return weapon_data.bullet_scene if weapon_data else null return weapon_data.bullet_scene if weapon_data else null
func get_weapon_resource() -> Resource: func get_weapon_resource() -> Resource:
return weapon_data return weapon_data
func select_weapon_by_name(weapon_name: String) -> bool: func select_weapon_by_name(weapon_name: String) -> bool:
for i in range(available_weapons.size()): for i in range(available_weapons.size()):
var candidate: WeaponShot = available_weapons[i] if available_weapons[i].shot_name == weapon_name:
if candidate != null and candidate.shot_name == weapon_name: _current_weapon_index = i
_cycled_weapon = candidate # override additive behavior weapon_data = available_weapons[i]
print("Switched to: ", weapon_name) print("Switched to: ", weapon_name)
_emit_weapon_changed()
_trigger_change_flash() _trigger_change_flash()
return true return true
return false return false
@ -194,36 +49,14 @@ func _trigger_change_flash() -> void:
effects_component.apply_effect(weapon_change_flash) 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(): if available_weapons.is_empty():
return return
_current_weapon_index = (_current_weapon_index + 1) % available_weapons.size() _current_weapon_index = (_current_weapon_index + 1) % available_weapons.size()
var new_weapon: WeaponShot = available_weapons[_current_weapon_index] weapon_data = available_weapons[_current_weapon_index]
if new_weapon == null: print("Switched to: ", weapon_data.shot_name)
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() _emit_weapon_changed()
_trigger_change_flash() _trigger_change_flash()

View file

@ -11,9 +11,6 @@ extends Resource
# PackedScene reference to the bullet / projectile scene instantiated on fire. # PackedScene reference to the bullet / projectile scene instantiated on fire.
@export var bullet_scene: PackedScene @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") @export_subgroup("Launch")
@ -85,20 +82,3 @@ extends Resource
# Array of projectile groups defining layered, multi-directional fire patterns. # Array of projectile groups defining layered, multi-directional fire patterns.
@export var groups: Array[WeaponProjectileGroup] = [] @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