Compare commits

...
Sign in to create a new pull request.

14 commits

Author SHA1 Message Date
1ac71cc104 Updated exported weapon variables for cleaner organization in the Godot
inspector.
2026-06-29 14:59:40 +01:00
fa882502b4 Updated comments for clarity. 2026-06-28 18:27:47 +01:00
a655f9332f Further refinements in weapone fire helper logic. 2026-06-28 18:11:26 +01:00
9554af183c Updated the bullet spawning position to beging from the sprite height
modified by the weapon's origin variable.
2026-06-28 08:05:30 +01:00
64e685a3c1 Still a lille buggy! But closer. 2026-06-28 02:38:02 +01:00
99a5d9838d Separated shot positioning math to helper utility and refactored shoot
component - introduced shot origin issue.
2026-06-28 01:45:11 +01:00
6c5a0a0bae Adjusted directory structure for clarity. 2026-06-27 13:28:38 +01:00
3303d937fd Added grouped shots as sub resources, as well as the abilit to mirror
pairs.
2026-06-26 23:54:53 +01:00
313e013543 Slight tweak to remove warnings. 2026-06-26 22:59:52 +01:00
2db5f37975 Added follow angle for spread shots so projectiles point in the
direction they're going.
2026-06-26 22:55:32 +01:00
62e02d2555 Weapon feel modifications. 2026-06-26 12:31:19 +01:00
122f7bed5f Further weapon feel modificaitons. 2026-06-25 11:37:55 +01:00
30619bc7c1 Fixed initial double shot issue; tweaked feel of weapons. 2026-06-25 11:35:42 +01:00
4f3839448b Modified weapon scripts and resources with a simplified spread angle
variable.
2026-06-25 02:06:28 +01:00
23 changed files with 437 additions and 68 deletions

BIN
graphics/spread.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwc5fjwhqikoo"
path="res://.godot/imported/spread.png-77a3efff77439dd55f282fc699502b2f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://graphics/spread.png"
dest_files=["res://.godot/imported/spread.png-77a3efff77439dd55f282fc699502b2f.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

@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="WeaponProjectileGroup" format=3 uid="uid://tdmd8jq8cm4y"]
[ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="1_mhej7"]
[resource]
script = ExtResource("1_mhej7")
pattern_name = "fwd"
bullet_count = 5
group_delay = 5.0
stagger_offset = 2.0

View file

@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="WeaponProjectileGroup" format=3 uid="uid://ch8qch6lx1mdx"]
[ext_resource type="Script" uid="uid://d0ios82vubijg" path="res://scripts/weapon_projectile_group.gd" id="1_m23g4"]
[resource]
script = ExtResource("1_m23g4")
spread_angle = 3.0
angle_offset = 8.0
horizontal_offset = 20.0
group_delay = 0.25
stagger_offset = 0.5
mirror = true

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://ds53sryglmf27"]
[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"]
[resource]
script = ExtResource("2_4b7mw")
shot_name = "spread"
bullet_scene = ExtResource("1_pn64g")
damage = 5
speed = 720
projectiles = 5
horizontal_offset = 8.25
spread_angle = 25.0
stagger_offset = 0.5
stagger_animation = true
metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -1,6 +1,7 @@
[gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://b75ae840k03dy"]
[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"]
[resource]
@ -8,9 +9,7 @@ script = ExtResource("2_ym48c")
shot_name = "stock"
bullet_scene = ExtResource("1_lcw6o")
damage = 2
speed = 300
spacing = 25.0
origin = -15
horizontal_offset = 10.0
stagger_offset = 0.35
speed = 650
horizontal_offset = 12.0
stagger_offset = 0.0
metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -1,6 +1,7 @@
[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="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"]
[resource]
@ -8,11 +9,9 @@ script = ExtResource("2_syw6j")
shot_name = "tri"
bullet_scene = ExtResource("1_07hyh")
damage = 3
speed = 420
speed = 700
projectiles = 3
spacing = 30.0
origin = -15
horizontal_offset = 8.5
stagger_offset = 0.2
horizontal_offset = 12.5
stagger_offset = 0.35
stagger_animation = true
metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="WeaponShot" format=3 uid="uid://c41aoyka7nfr8"]
[ext_resource type="PackedScene" uid="uid://ddpclu2vdy2ve" path="res://scenes/player_weapons/weapon_stock.tscn" id="1_lqlv8"]
[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"]
[resource]
script = ExtResource("3_fh2eq")
shot_name = "vanguard+"
bullet_scene = ExtResource("1_lqlv8")
damage = 5
speed = 500
projectiles = 5
horizontal_offset = 8.25
stagger_offset = 0.5
compensate_spawn_geometry = true
stagger_animation = true
follow_angle = true
groups = Array[ExtResource("2_qeb4b")]([ExtResource("3_qeb4b")])
metadata/_custom_type_script = "uid://7n1itonn35fm"

View file

@ -2,6 +2,7 @@
[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"]
[resource]
script = ExtResource("2_w7xlm")
@ -10,8 +11,6 @@ bullet_scene = ExtResource("1_hm46f")
damage = 5
speed = 500
projectiles = 5
spacing = 30.0
origin = -15
horizontal_offset = 8.25
stagger_offset = 0.5
stagger_animation = true

View file

@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://bq3xhbfgkt22l"]
[ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects/effects_component.gd" id="1_ee0kt"]
[ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects_component.gd" id="1_ee0kt"]
[node name="EffectsComponent" type="Node" unique_id=1393438576]
script = ExtResource("1_ee0kt")

View file

@ -10,9 +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/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://c41aoyka7nfr8" path="res://resources/player_weapon_resources/vanguard+.tres" id="14_oprun"]
[sub_resource type="AtlasTexture" id="AtlasTexture_tuyoq"]
atlas = ExtResource("5_qlg0r")
@ -149,6 +152,10 @@ player = NodePath("..")
speed = 200.0
metadata/_custom_type_script = "uid://c0rikbakpcags"
[node name="WeaponFireHelper" type="Node" parent="." unique_id=623642426]
unique_name_in_owner = true
script = ExtResource("15_firehelper")
[node name="ShootComponent" type="Node" parent="." unique_id=623642425]
unique_name_in_owner = true
script = ExtResource("6_y4r1p")
@ -157,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")])
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")
metadata/_custom_type_script = "uid://ylmao2ndp22y"

View file

@ -10,6 +10,9 @@ var time_offset: float = 0.5
# When this bullet should fire (absolute time)
var fire_time: float = 0.0
# Angle offset for spread shots (in degrees)
var angle: float = 0.0
# Track if this bullet is currently active in the shot
var is_active: bool = false
@ -20,8 +23,13 @@ func _process(delta):
if !is_active and current_time >= fire_time:
activate()
# Move the bullet
position.y -= shot_data.speed * delta
# Move the bullet every frame; stagger controls visibility, not movement
var velocity = Vector2.UP.rotated(deg_to_rad(angle)) * shot_data.speed
position += velocity * delta
# Rotate sprite to face travel direction when conditions are met
if shot_data.follow_angle and shot_data.projectiles > 2 and shot_data.spread_angle > 0:
$AnimatedSprite2D.rotation = deg_to_rad(angle)
func activate():
is_active = true

View file

@ -29,7 +29,7 @@ animations = [{
}],
"loop": 1,
"name": &"default",
"speed": 30.0
"speed": 24.0
}]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_mvdrj"]

View file

@ -22,7 +22,7 @@ func tick(delta: float):
# Calculate ship to bullet displacement
player.ship_displacement = player.position.y - player.previous_position.y
player.travel += abs(weapon_system.speed) * delta + (player.ship_displacement)
player.travel = clamp(player.travel, 0, 1.9 * weapon_system.spacing)
player.travel = clamp(player.travel, 0, 1.9 * weapon_system.projectile_vertical_spacing)
# Thruster animation in relation to player movement
if input.x > 0:

View file

@ -2,48 +2,89 @@ class_name ShootComponent extends Node
@onready var weapon_component: Node = %WeaponComponent
@onready var player = $".."
@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
var total_projectiles = weapon_data.projectiles
if player.travel > weapon_data.spacing:
if player.travel > weapon_data.projectile_vertical_spacing:
_shoot_standard(weapon_data, current_time)
_shoot_grouped(weapon_data, current_time)
# Calculate center index for symmetrical staggering
var center_index = (total_projectiles - 1) / 2.0
# Reset travel after firing to prevent multiple bursts per tap
player.travel = 0.0
if total_projectiles > 0:
for b in range(total_projectiles):
var bullet := weapon_data.bullet_scene.instantiate() as Area2D
get_tree().root.add_child(bullet)
# Calculate index relative to center (0 = center, -1/+1 = first from center)
var index_from_center: float = b - center_index
func _get_spawn_position(weapon_data: WeaponShot) -> Vector2:
# Determine sprite height with a region_rect → texture fallback.
var sprite_height: float = 0.0
if ship.region_rect.has_area():
sprite_height = ship.region_rect.size.y
else:
sprite_height = ship.texture.get_height()
# Calculate timing offset for staggered firing
var time_offset: float = index_from_center * weapon_data.stagger_offset
# Top edge of the sprite in world space, then offset by origin.
var top_edge = ship.global_position.y - (sprite_height / 2.0)
return Vector2(ship.global_position.x, top_edge + weapon_data.origin)
# Calculate horizontal offset for this bullet (symmetrical: left=-ve, right=+ve)
var bullet_horizontal_offset: float = index_from_center * weapon_data.horizontal_offset
# Calculate vertical offset from center point (symmetrical vertical spread)
var distance_from_center: float = abs(index_from_center)
var vertical_offset: float = (distance_from_center * weapon_data.origin * -1) / 2
# Standard projectile logic (delegates to WeaponFireHelper)
func _shoot_standard(weapon_data: WeaponShot, current_time: float):
var total_projectiles: int = weapon_data.projectiles
# Final position combines symmetrical horizontal spread with symmetrical vertical spacing
bullet.position = player.position + Vector2(bullet_horizontal_offset, weapon_data.origin + (vertical_offset + weapon_data.origin - total_projectiles))
if total_projectiles > 0:
for b in range(total_projectiles):
var bullet := weapon_data.bullet_scene.instantiate() as Area2D
get_tree().root.add_child(bullet)
# Set timing properties on the bullet
bullet.time_offset = time_offset
bullet.fire_time = current_time + time_offset
# 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)
bullet.position = props.position
bullet.angle = props.angle
bullet.time_offset = props.time_offset
bullet.fire_time = props.fire_time
# Set the bullet data
bullet.set_weapon_data(weapon_data.bullet_scene, weapon_data)
# Set the bullet data
bullet.set_weapon_data(weapon_data.bullet_scene, weapon_data)
# Offset animation playback per projectile if enabled
# Offset animation playback per projectile if enabled
if weapon_data.stagger_animation:
bullet.set_animation_offset(props.animation_offset)
# Grouped projectile logic (delegates to WeaponFireHelper)
func _shoot_grouped(weapon_data: WeaponShot, current_time: float):
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)
bullet.position = props.position
bullet.angle = props.angle
bullet.time_offset = props.time_offset
bullet.fire_time = props.fire_time
bullet.set_weapon_data(weapon_data.bullet_scene, weapon_data)
if weapon_data.stagger_animation:
bullet.set_animation_offset(props.animation_offset)
# If mirroring is enabled, fire a mirrored copy
if group.mirror:
var mirror_bullet := weapon_data.bullet_scene.instantiate() as Area2D
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)
mirror_bullet.position = mirror_props.position
mirror_bullet.angle = mirror_props.angle
mirror_bullet.time_offset = mirror_props.time_offset
mirror_bullet.fire_time = mirror_props.fire_time
mirror_bullet.set_weapon_data(weapon_data.bullet_scene, weapon_data)
if weapon_data.stagger_animation:
bullet.set_animation_offset(index_from_center * weapon_data.stagger_offset)
# Subtract projectile spacing from current Player travel for next shot
player.travel -= weapon_data.spacing
mirror_bullet.set_animation_offset(mirror_props.animation_offset)

View file

@ -21,7 +21,7 @@ func _ready() -> void:
func _emit_weapon_changed() -> void:
var ship: Sprite2D = get_parent().get_node("Ship")
var sprite_size = ship.get_rect().size
var origin_offset = weapon_data.origin if weapon_data else 0.0
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)
@ -31,12 +31,12 @@ func get_bullet_scene() -> PackedScene:
func get_weapon_resource() -> Resource:
return weapon_data
func select_weapon_by_name(name: String) -> bool:
func select_weapon_by_name(weapon_name: String) -> bool:
for i in range(available_weapons.size()):
if available_weapons[i].shot_name == name:
if available_weapons[i].shot_name == weapon_name:
_current_weapon_index = i
weapon_data = available_weapons[i]
print("Switched to: ", name)
print("Switched to: ", weapon_name)
_emit_weapon_changed()
_trigger_change_flash()

View file

@ -0,0 +1,129 @@
class_name WeaponFireHelper extends Node
## Compute bullet properties for a standard shot.
## Returns a Dictionary with: position, angle, fire_time, time_offset, index_from_center, animation_offset
func compute_standard_props(weapon_data: WeaponShot, projectile_index: int, spawn_position: Vector2, current_time: float) -> Dictionary:
var total_projectiles: int = weapon_data.projectiles
var center_index: float = (total_projectiles - 1) / 2.0
var index_from_center: float = projectile_index - center_index
# Horizontal offset (symmetrical: left=-ve, right=+ve)
var bullet_horizontal_offset: float = index_from_center * weapon_data.horizontal_offset
# Timing: center-first, pairs outward
var time_offset: float = abs(index_from_center) * weapon_data.stagger_offset
# Angle (only when projectiles >= 3, division-by-zero guard)
# Moved before vertical offset to support compensation
var bullet_angle: float = 0.0
if total_projectiles >= 3:
bullet_angle = (index_from_center / center_index) * (weapon_data.spread_angle / 2.0)
# Vertical spacing creates the ^ (inverted-V) geometry: outer projectiles fire lower
# (higher Y values, further from the player) than the center projectile, which sits
# higher on screen (lower Y values). This creates a ^ pattern relative to the origin.
# When compensate_spawn_geometry is enabled and spread_angle > 0, compute vertical
# offsets to align the geometric spread with velocity vectors.
var vertical_offset: float
if weapon_data.compensate_spawn_geometry and weapon_data.spread_angle > 0 and total_projectiles >= 3:
var velocity_angle_rad: float = abs(bullet_angle) * PI / 180.0
if index_from_center == 0:
# Center bullet in compensated mode: use full projectile_vertical_spacing
# so the V-shape stays visually smooth (center = 35, outer ≈ 75)
# instead of a broken gap (center = 8.75, outer ≈ 75).
vertical_offset = weapon_data.projectile_vertical_spacing
elif velocity_angle_rad > 0.1: # Guard against division by zero for very small angles
vertical_offset = abs(bullet_horizontal_offset) / tan(velocity_angle_rad)
else:
# Very small angle: fall back to full spacing
vertical_offset = weapon_data.projectile_vertical_spacing
else:
vertical_offset = (abs(index_from_center) * weapon_data.projectile_vertical_spacing) / 2.0
var position: Vector2 = spawn_position + Vector2(bullet_horizontal_offset, +vertical_offset)
# Fire time
var fire_time: float = current_time + time_offset
# Animation offset (if staggered animation is enabled)
var animation_offset: float = index_from_center * weapon_data.stagger_offset
return {
"position": position,
"angle": bullet_angle,
"fire_time": fire_time,
"time_offset": time_offset,
"index_from_center": index_from_center,
"animation_offset": animation_offset,
}
## Compute bullet properties for a grouped shot, with optional mirroring.
## Returns a Dictionary with: position, angle, fire_time, time_offset, index_from_center, animation_offset
func compute_grouped_props(group: WeaponProjectileGroup, weapon_data: WeaponShot,
bullet_index: int, spawn_position: Vector2, current_time: float,
mirror: bool = false) -> Dictionary:
var center_index: float = (group.bullet_count - 1) / 2.0
var index_from_center: float = bullet_index - center_index
# Horizontal offset: group offset + within-group spread
var bullet_horizontal_offset: float = group.horizontal_offset + index_from_center * group.horizontal_spacing
if mirror:
bullet_horizontal_offset = -bullet_horizontal_offset
# Base angle: group angle (negated when mirrored) + within-group spread
var bullet_angle: float = group.angle_offset
if mirror:
bullet_angle = -group.angle_offset
# Symmetrical spread (guard against < 3 bullets)
var spread_contribution: float = 0.0
if group.bullet_count >= 3:
spread_contribution = (index_from_center / center_index) * (group.spread_angle / 2.0)
if mirror:
bullet_angle -= spread_contribution
else:
bullet_angle += spread_contribution
# Vertical spacing creates the ^ (inverted-V) geometry: outer projectiles fire lower
# (higher Y values, further from the player) than the center projectile, which sits
# higher on screen (lower Y values). This creates a ^ pattern relative to the origin.
# When compensate_spawn_geometry is enabled and group.spread_angle > 0, compute
# vertical offsets to preserve the within-group V-shape independently of
# angle_offset, which handles global tilt.
#
# angle_offset tilts the entire group — mixing it into the compensation formula
# breaks the V-shape because the formula assumes velocity vectors converge at the
# origin, but with angle_offset > 0 they fan out from an offset line instead.
# Solution: calculate compensation using only within-group parameters.
var vertical_offset: float
if weapon_data.compensate_spawn_geometry and group.spread_angle > 0 and group.bullet_count >= 3 and index_from_center != 0:
var within_group_angle: float = abs(spread_contribution) # exclude angle_offset
var within_group_angle_rad: float = within_group_angle * PI / 180.0
var within_group_horizontal: float = abs(index_from_center * group.horizontal_spacing) # exclude group.horizontal_offset
if within_group_angle_rad > 0.1: # Guard against division by zero for very small angles
vertical_offset = within_group_horizontal / tan(within_group_angle_rad)
else:
vertical_offset = (abs(index_from_center) * weapon_data.projectile_vertical_spacing) / 2.0
else:
vertical_offset = (abs(index_from_center) * weapon_data.projectile_vertical_spacing) / 2.0
var position: Vector2 = spawn_position + Vector2(bullet_horizontal_offset, +vertical_offset)
# Timing: group delay + within-group stagger
var time_offset: float = group.group_delay + abs(index_from_center) * group.stagger_offset
# Fire time
var fire_time: float = current_time + time_offset
# Animation offset (if staggered animation is enabled)
var animation_offset: float = index_from_center * weapon_data.stagger_offset
return {
"position": position,
"angle": bullet_angle,
"fire_time": fire_time,
"time_offset": time_offset,
"index_from_center": index_from_center,
"animation_offset": animation_offset,
}

View file

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

View file

@ -0,0 +1,23 @@
class_name WeaponProjectileGroup
extends Resource
@export_category("Identification")
@export var pattern_name: String = ""
@export_category("Group Settings")
@export var bullet_count: int = 3
# Horizontal distance between bullets in this group
@export var horizontal_spacing: float = 6.5
# Total spread angle for bullets in this group (in degrees)
@export var spread_angle: float = 0.0
# Angle of the entire group from the shot origin (positive = right/up, negative = left)
@export var angle_offset: float = 0.0
# Horizontal offset from the center line (positive = right, negative = left)
@export var horizontal_offset: float = 0.0
@export_category("Timing")
# Time delay for this group's first bullet (relative to shot start)
@export var group_delay: float = 0.0
# Stagger delay between bullets within this group
@export var stagger_offset: float = 0.25
@export_category("Mirroring")
# If true, duplicate this group's bullets mirrored across the ship's vertical axis
@export var mirror: bool = false

View file

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

View file

@ -1,23 +1,84 @@
class_name WeaponShot
extends Resource
@export_category("Parameters")
@export_group("Weapon")
# Human-readable name for reference in UI and logs.
@export var shot_name: String
@export var bullet_scene: PackedScene = null
@export var damage: int = 1
@export var speed: int = 135
@export var projectiles: int = 2
# Vertical spacing between projectiles
@export var spacing: float = 35
# Origin offset from center of player
@export var origin: int = -23
@export_category("Spread")
# Horizontal distance between projectiles
@export var horizontal_offset: float = 6.5
# Time delay per projectile index
@export var stagger_offset: float = 0.25
# PackedScene reference to the bullet / projectile scene instantiated on fire.
@export var bullet_scene: PackedScene
@export_category("Effects")
# Offset animation playback per projectile index
@export_subgroup("Launch")
# Damage dealt by a single projectile instance.
@export_range(1, 100, 1) var damage: int = 1
# Travel speed of projectiles in pixels per second.
@export_range(50, 2000, 10) var speed: int = 135
# Number of projectiles fired in a burst pattern.
@export_range(1, 20, 1) var projectiles: int = 2
@export_subgroup("Spawn Geometry")
# Vertical spacing between projectiles in a burst (used by WeaponFireHelper to compute the ^ pattern).
# This parameter has a dual role: spatial (computing the ^ burst geometry) and temporal
# (serving as the burst-gating threshold in ShootComponent.shoot()). When compensate_spawn_geometry
# is enabled, this parameter is overridden by geometric compensation for spatial calculations;
# temporal behavior remains unchanged and continues to use this value.
@export_range(10, 200, 0.5) var projectile_vertical_spacing: float = 35
# Vertical offset from the top of the player's sprite where bullets spawn.
# Measured in pixels; negative values fire in front of (above) the ship,
# positive values fire behind (below) it. The default of -25 places the
# burst 25 pixels above the sprite's top edge.
@export_range(-200, 200, 0.5) var origin: float = -25.0
@export_subgroup("Spread")
# Horizontal distance between adjacent projectiles in a burst.
@export_range(0, 100, 0.25) var horizontal_offset: float = 6.5
# Total spread angle in degrees (only applied when projectiles >= 3).
@export_range(0, 360, 0.5) var spread_angle: float = 0.0
# Time delay per projectile index in a burst.
@export_range(0.01, 5.0, 0.05) var stagger_offset: float = 0.25
# When enabled, adjusts spawn vertical offsets so the geometric line
# from the center bullet to each outer bullet matches their velocity
# direction. Eliminates the optical illusion where the spread appears
# wider than the actual bullet trajectories (e.g. vanguard+). See the
# Debugging Checklist for the vertical offset pitfall on standard shots.
## WARNING: This flag uses a trigonometric formula (horizontal_offset / tan(angle)) that evaluates independently-tuned horizontal and spread angles in grouped shots ONLY!
@export var compensate_spawn_geometry: bool = false
@export_subgroup("Timing")
# Offset animation playback per projectile index.
@export var stagger_animation: bool = false
@export_subgroup("Effects")
# Rotate projectiles to point along their travel direction (requires >2 projectiles and spread_angle > 0).
@export var follow_angle: bool = false
@export_subgroup("Groups")
# Array of projectile groups defining layered, multi-directional fire patterns.
@export var groups: Array[WeaponProjectileGroup] = []