Separated shot positioning math to helper utility and refactored shoot

component - introduced shot origin issue.
This commit is contained in:
Henry Faber 2026-06-28 01:45:11 +01:00
parent 6c5a0a0bae
commit 99a5d9838d
17 changed files with 147 additions and 98 deletions

View file

@ -4,6 +4,7 @@
[resource]
script = ExtResource("1_mhej7")
pattern_name = "fwd"
bullet_count = 5
group_delay = 5.0
stagger_offset = 2.0

View file

@ -2,6 +2,7 @@
[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")
@ -10,8 +11,6 @@ bullet_scene = ExtResource("1_pn64g")
damage = 5
speed = 720
projectiles = 5
spacing = 100.0
origin = -15
horizontal_offset = 8.25
spread_angle = 25.0
stagger_offset = 0.5

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]
@ -9,8 +10,6 @@ shot_name = "stock"
bullet_scene = ExtResource("1_lcw6o")
damage = 2
speed = 650
spacing = 25.0
origin = -15
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]
@ -10,8 +11,6 @@ bullet_scene = ExtResource("1_07hyh")
damage = 3
speed = 700
projectiles = 3
spacing = 45.0
origin = -15
horizontal_offset = 12.5
stagger_offset = 0.35
stagger_animation = true

View file

@ -12,8 +12,6 @@ bullet_scene = ExtResource("1_lqlv8")
damage = 5
speed = 500
projectiles = 5
spacing = 30.0
origin = -15
horizontal_offset = 8.25
stagger_offset = 0.5
stagger_animation = true

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

@ -10,6 +10,7 @@
[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"]
@ -151,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")

View file

@ -2,6 +2,6 @@ extends Node
@warning_ignore_start("unused_signal") # since otherwise Godot will throw a warning that the signal is unused in current scope
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: Vector2)
@warning_ignore_restore("unused_signal")

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

@ -6,7 +6,7 @@ class_name NotificationDisplay extends Control
var _player: Node2D = null
var _sprite_size: Vector2 = Vector2.ZERO
var _origin_offset: float = 0.0
var _origin_offset: Vector2 = Vector2.ZERO
var _follow_player: bool = false
var _connector_line: Line2D = null
@ -23,7 +23,7 @@ func _ready() -> void:
_container.add_theme_constant_override("margin_right", 4)
func show_notification(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: float) -> void:
func show_notification(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: Vector2) -> void:
modulate.a = 0.0
label.add_theme_font_size_override("font_size", 8)
@ -107,7 +107,7 @@ func _update_position() -> void:
var player_pos = _player.position
# Y position uses the weapon origin offset from player center
var notif_y = player_pos.y + _origin_offset - (size.y / 2.0)
var notif_y = player_pos.y + _origin_offset.y - (size.y / 2.0)
# X position is 10px to the right of player's sprite edge
var right_edge = player_pos.x + (_sprite_size.x / 2.0)
@ -164,7 +164,7 @@ func _update_connector() -> void:
# Point 2: player's origin + ENDPOINT_OFFSET, converted to notification's local space
const ENDPOINT_OFFSET = Vector2(4, 7) # offset from player's origin to connector endpoint
var target = _player.position + Vector2(+ENDPOINT_OFFSET.x, _origin_offset + ENDPOINT_OFFSET.y)
var target = _player.position + Vector2(+ENDPOINT_OFFSET.x, _origin_offset.y + ENDPOINT_OFFSET.y)
var point2 = target - position
_connector_line.points = PackedVector2Array([point0, point1, point2])

View file

@ -8,7 +8,7 @@ func _ready() -> void:
EventBus.weapon_changed.connect(_on_weapon_changed)
func _on_weapon_changed(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: float) -> void:
func _on_weapon_changed(weapon_name: String, player_node: Node2D, player_sprite_size: Vector2, origin_offset: Vector2) -> void:
if notification_scene == null:
return

View file

@ -2,12 +2,14 @@ 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
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)
@ -15,105 +17,61 @@ func shoot():
player.travel = 0.0
# Standard projectile logic (backward compatible)
# Standard projectile logic (delegates to WeaponFireHelper)
func _shoot_standard(weapon_data: WeaponShot, current_time: float):
var total_projectiles = weapon_data.projectiles
var center_index = (total_projectiles - 1) / 2.0
var total_projectiles: int = weapon_data.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)
# Calculate index relative to center (0 = center, -1/+1 = first from center)
var index_from_center: float = b - center_index
# Calculate timing offset for staggered firing (center-first, pairs outward)
var time_offset: float = abs(index_from_center) * weapon_data.stagger_offset
# 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
# 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))
# Calculate angle for this bullet (only when projectiles >= 3)
var bullet_angle: float = 0.0
if total_projectiles >= 3:
bullet_angle = (index_from_center / center_index) * (weapon_data.spread_angle / 2.0)
# Set timing properties on the bullet
bullet.time_offset = time_offset
bullet.fire_time = current_time + time_offset
bullet.angle = bullet_angle
# Delegate math to the helper, with spawn position offset by sprite height and weapon origin
var spawn_position = ship.global_position + Vector2(0, ship.get_texture().get_height() / 2.0) + weapon_data.origin
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)
# Offset animation playback per projectile if enabled
if weapon_data.stagger_animation:
bullet.set_animation_offset(index_from_center * weapon_data.stagger_offset)
bullet.set_animation_offset(props.animation_offset)
# Grouped projectile logic (new functionality)
# 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)
_fire_grouped_bullet(bullet, group, b, current_time, weapon_data, false)
# Apply computed properties for the original bullet
var spawn_position = ship.global_position + Vector2(0, ship.get_texture().get_height() / 2.0) + weapon_data.origin
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)
_fire_grouped_bullet(mirror_bullet, group, b, current_time, weapon_data, true)
# Fire a single grouped bullet (original or mirrored)
func _fire_grouped_bullet(bullet: Area2D, group: WeaponProjectileGroup, b: int,
current_time: float, weapon_data: WeaponShot, mirror: bool):
# Calculate index relative to this group's center
var center_index: float = (group.bullet_count - 1) / 2.0
var index_from_center: float = b - center_index
# Horizontal offset: group offset + within-group spread
# Mirror: negate the horizontal offset contribution
var bullet_horizontal_offset: float = group.horizontal_offset + index_from_center * group.horizontal_spacing
if mirror:
bullet_horizontal_offset = -bullet_horizontal_offset
# Base angle for the group, plus within-group spread
# Mirror: negate the angle and the spread contribution
var bullet_angle: float = group.angle_offset
if mirror:
bullet_angle = -group.angle_offset
if group.bullet_count >= 3:
var spread_contribution = (index_from_center / center_index) * (group.spread_angle / 2.0)
if mirror:
bullet_angle -= spread_contribution
else:
bullet_angle += spread_contribution
# Position: apply horizontal offset and vertical origin from weapon data
bullet.position = player.position + Vector2(bullet_horizontal_offset, weapon_data.origin)
# Timing: group delay + within-group stagger
var time_offset: float = group.group_delay + abs(index_from_center) * group.stagger_offset
# Set timing properties on the bullet
bullet.time_offset = time_offset
bullet.fire_time = current_time + time_offset
bullet.angle = bullet_angle
# Set the bullet data
bullet.set_weapon_data(weapon_data.bullet_scene, weapon_data)
# Offset animation playback per projectile if enabled
if weapon_data.stagger_animation:
bullet.set_animation_offset(index_from_center * weapon_data.stagger_offset)
# Apply computed properties for the mirrored bullet
var mirror_spawn = ship.global_position + Vector2(0, ship.get_texture().get_height() / 2.0) + weapon_data.origin
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:
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: int = weapon_data.origin if weapon_data else 0
var origin_offset: Vector2 = weapon_data.origin if weapon_data else Vector2.ZERO
EventBus.weapon_changed.emit(weapon_data.shot_name, get_parent(), sprite_size, origin_offset)

View file

@ -0,0 +1,88 @@
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
# Vertical offset: outer projectiles are spaced relative to center, preserving the pattern geometry.
var vertical_offset: float = (abs(index_from_center) * weapon_data.projectile_vertical_spacing) / 2.0
var position: Vector2 = spawn_position + Vector2(bullet_horizontal_offset, vertical_offset)
# Angle (only when projectiles >= 3, division-by-zero guard)
var bullet_angle: float = 0.0
if total_projectiles >= 3:
bullet_angle = (index_from_center / center_index) * (weapon_data.spread_angle / 2.0)
# 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)
if group.bullet_count >= 3:
var spread_contribution = (index_from_center / center_index) * (group.spread_angle / 2.0)
if mirror:
bullet_angle -= spread_contribution
else:
bullet_angle += spread_contribution
# Position: horizontal offset only (vertical position comes from spawn_position)
var position: Vector2 = spawn_position + Vector2(bullet_horizontal_offset, 0)
# 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

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

View file

@ -7,10 +7,10 @@ extends Resource
@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
# Vertical spacing between projectiles when firing continuously
@export var projectile_vertical_spacing: float = 35
# Spawn offset from center of player (x is usually 0, y controls vertical fire position)
@export var origin: Vector2 = Vector2(0, -25)
@export_category("Spread")
# Horizontal distance between projectiles