Improves lightning projectile behavior

Refactors the lightning projectile to handle collisions more effectively. It now stops upon hitting an enemy or object, triggers an explosion, and spawns child projectiles from the collision point.  The lightning bolt's collision shape is dynamically updated to match the remaining distance to the target or the collision point. Also adds more test enemies to the map for testing purposes.
This commit is contained in:
Dan Baker 2025-05-06 09:30:44 +01:00
parent d0c2a7b3c8
commit 1a959fbc0c
7 changed files with 232 additions and 41 deletions

View file

@ -1,14 +1,14 @@
extends Node2D
class_name LightningBolt
var goal_point: Vector2 = Vector2(100, 100)
var min_segment_size: float = 2
var max_segment_size: float = 10
var points: Array = []
var emitting = true
var final_goal: Vector2
@export var angle_var: float = 15
@onready var line: Line2D = $Line2D
func _ready():
@ -18,34 +18,37 @@ func _ready():
$Timer.start(randf_range(0.1, 0.5))
func _on_timer_timeout():
Log.pr("Timer timeout")
if (points.size() > 0):
points.pop_front()
line.points = points
#Small variation for more organic look:
$Timer.start(0.002 + randf_range(-0.001, 0.001))
elif (emitting):
update_points()
line.points = points
$Timer.start(0.1 + randf_range(-0.02, 0.1))
func update_points():
final_goal = goal_point - global_position
var curr_line_len = 0
points = [Vector2()]
var start_point = Vector2()
min_segment_size = max(Vector2().distance_to(final_goal) / 40, 1)
max_segment_size = min(Vector2().distance_to(final_goal) / 20, 10)
while (curr_line_len < Vector2().distance_to(final_goal)):
# Adjust segment size based on the distance to goal
var distance_to_goal = Vector2().distance_to(final_goal)
min_segment_size = max(distance_to_goal / 40, 1)
max_segment_size = min(distance_to_goal / 20, 10)
while (curr_line_len < distance_to_goal):
var move_vector = start_point.direction_to(final_goal) * randf_range(min_segment_size, max_segment_size)
var new_point = start_point + move_vector
var new_point_rotated = start_point + move_vector.rotated(deg_to_rad(randf_range(-angle_var, angle_var)))
points.append(new_point_rotated)
start_point = new_point
curr_line_len = start_point.length()
points.append(final_goal)
func set_line_width(amount):
line.width = amount
line.width = amount

View file

@ -4,22 +4,23 @@
[ext_resource type="PackedScene" uid="uid://cafaf3en63bp6" path="res://assets/projectiles/components/bolt.tscn" id="2_gc60m"]
[node name="ProjectileLightning" type="Area2D"]
collision_layer = 16
collision_mask = 12
script = ExtResource("1_2ex40")
[node name="Bolt" parent="." instance=ExtResource("2_gc60m")]
modulate = Color(0.161899, 0.42984, 0.777414, 1)
rotation = 0.872665
[node name="Bolt2" parent="." instance=ExtResource("2_gc60m")]
modulate = Color(0.443066, 0.672326, 1, 1)
rotation = 0.872665
[node name="Bolt3" parent="." instance=ExtResource("2_gc60m")]
modulate = Color(0.205858, 0.510414, 0.999999, 1)
rotation = 0.349066
[node name="Bolt4" parent="." instance=ExtResource("2_gc60m")]
modulate = Color(1, 1, 1, 0.596078)
[node name="Bolt5" parent="." instance=ExtResource("2_gc60m")]
modulate = Color(1, 1, 1, 0.603922)
[node name="Bolt6" parent="." instance=ExtResource("2_gc60m")]

View file

@ -22,3 +22,15 @@ position = Vector2(20, 20)
[node name="TestEnemy" parent="." instance=ExtResource("4_raxr4")]
position = Vector2(87, 72)
[node name="TestEnemy2" parent="." instance=ExtResource("4_raxr4")]
position = Vector2(171, 38)
[node name="TestEnemy3" parent="." instance=ExtResource("4_raxr4")]
position = Vector2(132, 150)
[node name="TestEnemy4" parent="." instance=ExtResource("4_raxr4")]
position = Vector2(199, 107)
[node name="TestEnemy5" parent="." instance=ExtResource("4_raxr4")]
position = Vector2(272, 64)

View file

@ -5,20 +5,12 @@ var player: CharacterBody2D
var animated_sprite: AnimatedSprite2D
func process(_delta):
# Get mouse position in global coordinates
var mouse_position = player.get_global_mouse_position()
# Get player position (assuming this script is attached to the player)
var player_position = player.global_position
# Calculate direction vector from player to mouse
var direction = mouse_position - player_position
# You can normalize this vector if you want a unit vector (length of 1)
# This is useful if you only care about direction, not distance
var normalized_direction = direction.normalized()
if Input.is_action_pressed("fire"):
var mouse_position = player.get_global_mouse_position()
var player_position = player.global_position
var direction = mouse_position - player_position
var normalized_direction = direction.normalized()
player.weapon.fire(normalized_direction, mouse_position)
# Update animation
#update_animation()

View file

@ -15,7 +15,7 @@ signal on_destroyed(projectile)
# Modifier-related properties
var pierce_count: int = 0
var has_explosive_impact: bool = true
var explosion_projectile_count: int = 2
var explosion_projectile_count: int = 3
var explosion_projectile_damage_mult: float = 0.5
var explosion_projectile_speed: float = 300.0
var explosion_spread_angle: float = 360.0 # Full circle by default
@ -24,13 +24,13 @@ var explosion_spread_angle: float = 360.0 # Full circle by default
var source_weapon: RangedWeapon # Reference to the weapon that fired this
var lifetime_timer: Timer
# Add a variable to track the entity that triggered the explosion
var ignore_target = null
var ignore_target = []
func _on_body_entered(body):
# Check if this is a body we should ignore
if body == ignore_target:
if ignore_target.has(body):
Log.pr("Ignoring body: ", body.name)
return
if body.is_in_group("enemies") and is_friendly:
Log.pr("Hit enemy: ", body.name)
# Deal damage to enemy
@ -47,7 +47,7 @@ func _on_body_entered(body):
# Handle explosive impact
if has_explosive_impact:
# Store the target that triggered the explosion
ignore_target = body
ignore_target.append(body)
_trigger_explosion()
# Destroy the projectile
@ -65,7 +65,9 @@ func _trigger_explosion():
func _spawn_explosion_projectiles():
for i in range(explosion_projectile_count):
# Create a new projectile
Log.pr("Spawning explosion projectile")
var new_proj = duplicate()
Log.pr("New projectile: ", new_proj)
new_proj.global_position = global_position
# Calculate new direction based on spread
@ -87,6 +89,10 @@ func _spawn_explosion_projectiles():
# Add to scene tree
get_tree().root.call_deferred("add_child", new_proj)
func set_projectile_scale(new_scale: Vector2):
# Set the scale of the projectile
self.scale = new_scale
func destroy():
emit_signal("on_destroyed", self)
queue_free()

View file

@ -1,30 +1,207 @@
class_name ProjectileLightning
extends ProjectileBase
@export var line_width: float = 1
@export var line_width: float = 2
@onready var lightning: Array = get_children()
# Add variables to track collision state
var has_collided: bool = false
var collision_point: Vector2 = Vector2.ZERO
func _init() -> void:
#super._init()
Log.pr("ProjectileLightning _init")
func _ready():
Log.pr(ignore_target)
lifetime_timer = Timer.new()
add_child(lifetime_timer)
lifetime_timer.one_shot = true
lifetime_timer.wait_time = lifetime
lifetime_timer.connect("timeout", _on_lifetime_timeout)
lifetime_timer.start()
# Make sure we have a collision shape
ensure_collision_shape()
for child in lightning:
if child.has_method("set_line_width"):
child.set_line_width(line_width)
emit_signal("on_spawned", self)
connect("body_entered", _on_body_entered)
func _process(_delta):
for child in lightning:
child.goal_point = target_position
# Add a method to ensure we have a collision shape
func ensure_collision_shape():
# Check if we already have a collision shape
var has_collision_shape = false
var existing_shape = null
for child in get_children():
if child is CollisionShape2D:
has_collision_shape = true
existing_shape = child
break
# If no collision shape exists, create one
if not has_collision_shape:
var collision_shape = CollisionShape2D.new()
var capsule_shape = CapsuleShape2D.new()
# Set the shape properties
capsule_shape.radius = line_width / 2
collision_shape.shape = capsule_shape
add_child(collision_shape)
existing_shape = collision_shape
# Make sure the lightning area is set to detect the right objects
set_collision_mask_value(2, true) # Assuming layer 2 is for enemies
set_collision_mask_value(3, true) # Assuming layer 3 is for objects
# Update the collision shape dimensions and position
update_collision_shape(existing_shape)
func _physics_process(delta):
position += direction * speed * delta
# Combined method to update collision shape based on current state
func update_collision_shape(collision_shape = null):
if collision_shape == null:
# Find the collision shape if not provided
for child in get_children():
if child is CollisionShape2D:
collision_shape = child
break
if collision_shape:
var capsule = collision_shape.shape as CapsuleShape2D
if capsule:
var target = collision_point if has_collided else target_position
var distance = global_position.distance_to(target)
var dir = (target - global_position).normalized()
# Update capsule height to match exact distance
capsule.height = distance
# Fix rotation based on current direction
collision_shape.rotation = dir.angle() + PI / 2
# Fix position: Move the shape so it starts at origin
collision_shape.position = dir * (distance / 2)
func _process(delta):
# Update target positions for lightning bolts
for child in lightning:
if child is LightningBolt:
child.goal_point = collision_point if has_collided else target_position
# Update collision shape if it exists
update_collision_shape()
# Implement the body_entered signal handler
func _on_body_entered(body):
if ignore_target.has(body):
Log.pr("Ignoring body: ", body.name)
return
# Check if the colliding body is an enemy or object
if body.is_in_group("enemies") or body.is_in_group("objects"):
if not has_collided: # Only process the first collision
# Set collision state and point
has_collided = true
# Calculate the collision point
var direction_to_body = (body.global_position - global_position).normalized()
var body_radius = 10.0 # Adjust for your game
collision_point = body.global_position - (direction_to_body * body_radius)
# Debug output
Log.pr("Lightning hit: " + body.name + " at point: " + str(collision_point))
# IMPORTANT: Immediately update the collision shape to stop at collision point
update_collision_shape()
_trigger_explosion()
#super._on_body_entered(body)
if body.is_in_group("enemies") and is_friendly:
Log.pr("Hit enemy: ", body.name)
# Deal damage to enemy
if body.has_method("take_damage"):
body.take_damage(damage)
# Emit signal for modifiers to react to
emit_signal("on_hit", self, body)
# Handle piercing
if pierce_count > 0:
pierce_count -= 1
else:
# Handle explosive impact
if has_explosive_impact:
# Store the target that triggered the explosion
ignore_target.append(body)
_trigger_explosion()
func _spawn_explosion_projectiles():
for i in range(explosion_projectile_count):
# Create a new projectile
Log.pr("Spawning explosion projectile")
var new_proj = (load(scene_file_path) as PackedScene).instantiate()
Log.pr("New projectile: ", new_proj)
new_proj.global_position = collision_point
var min_distance = 50
var max_distance = 125
# Generate a random angle
var max_target_distance = 200 # Maximum distance to look for enemies
# Get all enemies in the scene
var potential_targets = get_tree().get_nodes_in_group("enemies")
var valid_targets = []
# Filter enemies to only include ones within range
for enemy in potential_targets:
var distance = enemy.global_position.distance_to(collision_point)
if distance <= max_target_distance:
valid_targets.append(enemy)
var random_point = Vector2.ZERO
# Check if we have any valid targets
if valid_targets.size() > 0:
# Pick a random enemy from valid targets
var random_enemy = valid_targets[randi() % valid_targets.size()]
random_point = random_enemy.global_position
else:
# Fallback if no enemies in range - use the original random point logic
var random_angle = randf_range(0, TAU)
var random_distance = randf_range(50, max_target_distance)
random_point = collision_point + Vector2(
cos(random_angle) * random_distance,
sin(random_angle) * random_distance
)
new_proj.target_position = random_point
new_proj.damage = damage * explosion_projectile_damage_mult
new_proj.speed = explosion_projectile_speed
# Clear explosive properties so we don't get infinite loops
new_proj.has_explosive_impact = true
new_proj.explosion_projectile_count = 1
# Pass the ignore_target to the new projectiles
new_proj.ignore_target = ignore_target
Log.pr("New projectile: ", new_proj)
# Add to scene tree
get_tree().root.call_deferred("add_child", new_proj)
func _on_lifetime_timeout():
Log.pr("ProjectileLightning _on_lifetime_timeout")
super._on_lifetime_timeout()

View file

@ -10,7 +10,7 @@ var base_stats = {
"fire_rate": 2.0,
"projectile_speed": 500.0,
"projectile_size": 1.0,
"projectile_lifetime": 5.0,
"projectile_lifetime": 1.0,
"projectile_quantity": 1,
"projectile_spread": 33,
"max_pierce": 0
@ -84,7 +84,7 @@ func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target
# Set base size
var size = stats.get_stat("projectile_size")
projectile.scale = Vector2(size, size)
projectile.set_projectile_scale(Vector2(size, size))
# Allow modifiers to directly modify the projectile
for modifier in stats.modifiers: