Adds lightning projectile
Implements a lightning projectile with visual effects. The lightning is created using a series of bolt components that dynamically adjust their shape. Also refactors the projectile system to use a base class. Removes unused modifiers from player script.
This commit is contained in:
parent
ff62d67f54
commit
d0c2a7b3c8
12 changed files with 239 additions and 103 deletions
51
assets/projectiles/components/bolt.gd
Normal file
51
assets/projectiles/components/bolt.gd
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
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():
|
||||||
|
Log.pr("Bolt!")
|
||||||
|
line.width = 2
|
||||||
|
final_goal = goal_point - global_position
|
||||||
|
$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)):
|
||||||
|
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
|
||||||
1
assets/projectiles/components/bolt.gd.uid
Normal file
1
assets/projectiles/components/bolt.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bshl5y6qhgv2b
|
||||||
22
assets/projectiles/components/bolt.tscn
Normal file
22
assets/projectiles/components/bolt.tscn
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[gd_scene load_steps=4 format=3 uid="uid://cafaf3en63bp6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bshl5y6qhgv2b" path="res://assets/projectiles/components/bolt.gd" id="1_rrby1"]
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_sbfhf"]
|
||||||
|
_data = [Vector2(0.0154639, 0.409091), 0.0, 0.0, 0, 0, Vector2(0.448454, 0.981818), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 3
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_rrby1"]
|
||||||
|
offsets = PackedFloat32Array(0.0264901, 0.0993377, 0.84106, 1)
|
||||||
|
colors = PackedColorArray(1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0.865385, 1, 1, 1, 0)
|
||||||
|
|
||||||
|
[node name="Bolt" type="Node2D"]
|
||||||
|
script = ExtResource("1_rrby1")
|
||||||
|
|
||||||
|
[node name="Line2D" type="Line2D" parent="."]
|
||||||
|
width_curve = SubResource("Curve_sbfhf")
|
||||||
|
gradient = SubResource("Gradient_rrby1")
|
||||||
|
|
||||||
|
[node name="Timer" type="Timer" parent="."]
|
||||||
|
|
||||||
|
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
||||||
25
assets/projectiles/projectile_lightning.tscn
Normal file
25
assets/projectiles/projectile_lightning.tscn
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[gd_scene load_steps=3 format=3 uid="uid://cwn5eoynt6j48"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cb7fwabidqyj8" path="res://player/weapons/projectile_lightning.gd" id="1_2ex40"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cafaf3en63bp6" path="res://assets/projectiles/components/bolt.tscn" id="2_gc60m"]
|
||||||
|
|
||||||
|
[node name="ProjectileLightning" type="Area2D"]
|
||||||
|
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")]
|
||||||
|
|
||||||
|
[node name="Bolt5" parent="." instance=ExtResource("2_gc60m")]
|
||||||
|
|
||||||
|
[node name="Bolt6" parent="." instance=ExtResource("2_gc60m")]
|
||||||
|
|
@ -26,13 +26,13 @@ func _ready():
|
||||||
combat.animated_sprite = animated_sprite
|
combat.animated_sprite = animated_sprite
|
||||||
|
|
||||||
Log.pr("Adding projectile size additive modifier")
|
Log.pr("Adding projectile size additive modifier")
|
||||||
weapon.add_modifier(ProjectileSizeAdditive.new())
|
#weapon.add_modifier(ProjectileSizeAdditive.new())
|
||||||
Log.pr(weapon.stats.get_stat("projectile_size")) # Size is now 1.0 + 0.5 = 1.5
|
Log.pr(weapon.stats.get_stat("projectile_size")) # Size is now 1.0 + 0.5 = 1.5
|
||||||
# Size is now 1.0 + 0.5 = 1.5
|
# Size is now 1.0 + 0.5 = 1.5
|
||||||
|
|
||||||
# Add the multiplicative size modifier (1.5x multiplier)
|
# Add the multiplicative size modifier (1.5x multiplier)
|
||||||
Log.pr("Adding projectile size multiplicative modifier")
|
Log.pr("Adding projectile size multiplicative modifier")
|
||||||
weapon.add_modifier(ProjectileSizeMultiplicative.new())
|
#weapon.add_modifier(ProjectileSizeMultiplicative.new())
|
||||||
Log.pr(weapon.stats.get_stat("projectile_size"))
|
Log.pr(weapon.stats.get_stat("projectile_size"))
|
||||||
# Size is now 1.5 * 1.5 = 2.25
|
# Size is now 1.5 * 1.5 = 2.25
|
||||||
|
|
||||||
|
|
@ -40,10 +40,10 @@ func _ready():
|
||||||
Log.pr("Adding another projectile size additive modifier", 2)
|
Log.pr("Adding another projectile size additive modifier", 2)
|
||||||
var another_size_mod = ProjectileSizeAdditive.new()
|
var another_size_mod = ProjectileSizeAdditive.new()
|
||||||
another_size_mod.size_increase = 0.7
|
another_size_mod.size_increase = 0.7
|
||||||
weapon.add_modifier(another_size_mod)
|
#weapon.add_modifier(another_size_mod)
|
||||||
Log.pr(weapon.stats.get_stat("projectile_size"))
|
Log.pr(weapon.stats.get_stat("projectile_size"))
|
||||||
|
|
||||||
weapon.add_modifier(FireRateAdditive.new())
|
#weapon.add_modifier(FireRateAdditive.new())
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func process(_delta):
|
||||||
var normalized_direction = direction.normalized()
|
var normalized_direction = direction.normalized()
|
||||||
|
|
||||||
if Input.is_action_pressed("fire"):
|
if Input.is_action_pressed("fire"):
|
||||||
player.weapon.fire(normalized_direction)
|
player.weapon.fire(normalized_direction, mouse_position)
|
||||||
# Update animation
|
# Update animation
|
||||||
#update_animation()
|
#update_animation()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,5 @@
|
||||||
class_name Projectile extends Area2D
|
class_name Projectile
|
||||||
|
extends ProjectileBase
|
||||||
signal on_hit(projectile, target)
|
|
||||||
signal on_spawned(projectile)
|
|
||||||
signal on_destroyed(projectile)
|
|
||||||
|
|
||||||
@export var speed: float = 500.0
|
|
||||||
@export var damage: float = 10.0
|
|
||||||
@export var lifetime: float = 5.0
|
|
||||||
@export var direction: Vector2 = Vector2.RIGHT
|
|
||||||
@export var is_friendly: bool = true
|
|
||||||
|
|
||||||
# Modifier-related properties
|
|
||||||
var pierce_count: int = 0
|
|
||||||
var has_explosive_impact: bool = true
|
|
||||||
var explosion_projectile_count: int = 2
|
|
||||||
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
|
|
||||||
# References
|
|
||||||
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
|
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
lifetime_timer = Timer.new()
|
lifetime_timer = Timer.new()
|
||||||
|
|
@ -37,74 +15,5 @@ func _ready():
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
position += direction * speed * delta
|
position += direction * speed * delta
|
||||||
|
|
||||||
func _on_body_entered(body):
|
|
||||||
# Check if this is a body we should ignore
|
|
||||||
if body == ignore_target:
|
|
||||||
return
|
|
||||||
|
|
||||||
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 = body
|
|
||||||
_trigger_explosion()
|
|
||||||
|
|
||||||
# Destroy the projectile
|
|
||||||
destroy()
|
|
||||||
|
|
||||||
func _trigger_explosion():
|
|
||||||
# Create the explosion VFX
|
|
||||||
# var explosion = preload("res://scenes/explosion_effect.tscn").instantiate()
|
|
||||||
# explosion.global_position = global_position
|
|
||||||
# get_tree().root.add_child(explosion)
|
|
||||||
# Spawn the additional projectiles
|
|
||||||
if explosion_projectile_count > 0:
|
|
||||||
_spawn_explosion_projectiles()
|
|
||||||
|
|
||||||
func _spawn_explosion_projectiles():
|
|
||||||
# Calculate even angle distribution
|
|
||||||
var angle_step = explosion_spread_angle / explosion_projectile_count
|
|
||||||
var start_angle = - explosion_spread_angle / 2
|
|
||||||
|
|
||||||
for i in range(explosion_projectile_count):
|
|
||||||
# Create a new projectile
|
|
||||||
var new_proj = duplicate()
|
|
||||||
new_proj.global_position = global_position
|
|
||||||
|
|
||||||
# Calculate new direction based on spread
|
|
||||||
var random_angle = randf_range(0, 2 * PI)
|
|
||||||
var new_dir = Vector2.RIGHT.rotated(random_angle)
|
|
||||||
|
|
||||||
# Set properties for the new projectile
|
|
||||||
new_proj.direction = new_dir
|
|
||||||
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
|
|
||||||
|
|
||||||
# Add to scene tree
|
|
||||||
get_tree().root.add_child(new_proj)
|
|
||||||
|
|
||||||
func destroy():
|
|
||||||
emit_signal("on_destroyed", self)
|
|
||||||
queue_free()
|
|
||||||
|
|
||||||
func _on_lifetime_timeout():
|
func _on_lifetime_timeout():
|
||||||
destroy()
|
super._on_lifetime_timeout()
|
||||||
|
|
|
||||||
95
player/weapons/projectile_base.gd
Normal file
95
player/weapons/projectile_base.gd
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
class_name ProjectileBase
|
||||||
|
extends Area2D
|
||||||
|
|
||||||
|
signal on_hit(projectile, target)
|
||||||
|
signal on_spawned(projectile)
|
||||||
|
signal on_destroyed(projectile)
|
||||||
|
|
||||||
|
@export var speed: float = 500.0
|
||||||
|
@export var damage: float = 10.0
|
||||||
|
@export var lifetime: float = 5.0
|
||||||
|
@export var direction: Vector2 = Vector2.RIGHT
|
||||||
|
@export var target_position: Vector2
|
||||||
|
@export var is_friendly: bool = true
|
||||||
|
|
||||||
|
# Modifier-related properties
|
||||||
|
var pierce_count: int = 0
|
||||||
|
var has_explosive_impact: bool = true
|
||||||
|
var explosion_projectile_count: int = 2
|
||||||
|
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
|
||||||
|
|
||||||
|
# References
|
||||||
|
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
|
||||||
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
# Check if this is a body we should ignore
|
||||||
|
if body == ignore_target:
|
||||||
|
return
|
||||||
|
|
||||||
|
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 = body
|
||||||
|
_trigger_explosion()
|
||||||
|
|
||||||
|
# Destroy the projectile
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
func _trigger_explosion():
|
||||||
|
# Create the explosion VFX
|
||||||
|
# var explosion = preload("res://scenes/explosion_effect.tscn").instantiate()
|
||||||
|
# explosion.global_position = global_position
|
||||||
|
# get_tree().root.add_child(explosion)
|
||||||
|
# Spawn the additional projectiles
|
||||||
|
if explosion_projectile_count > 0:
|
||||||
|
_spawn_explosion_projectiles()
|
||||||
|
|
||||||
|
func _spawn_explosion_projectiles():
|
||||||
|
for i in range(explosion_projectile_count):
|
||||||
|
# Create a new projectile
|
||||||
|
var new_proj = duplicate()
|
||||||
|
new_proj.global_position = global_position
|
||||||
|
|
||||||
|
# Calculate new direction based on spread
|
||||||
|
var random_angle = randf_range(0, 2 * PI)
|
||||||
|
var new_dir = Vector2.RIGHT.rotated(random_angle)
|
||||||
|
|
||||||
|
# Set properties for the new projectile
|
||||||
|
new_proj.direction = new_dir
|
||||||
|
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
|
||||||
|
|
||||||
|
# Add to scene tree
|
||||||
|
get_tree().root.call_deferred("add_child", new_proj)
|
||||||
|
|
||||||
|
func destroy():
|
||||||
|
emit_signal("on_destroyed", self)
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _on_lifetime_timeout():
|
||||||
|
destroy()
|
||||||
1
player/weapons/projectile_base.gd.uid
Normal file
1
player/weapons/projectile_base.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c4bk8ac2ok2fx
|
||||||
30
player/weapons/projectile_lightning.gd
Normal file
30
player/weapons/projectile_lightning.gd
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
class_name ProjectileLightning
|
||||||
|
extends ProjectileBase
|
||||||
|
|
||||||
|
@export var line_width: float = 1
|
||||||
|
@onready var lightning: Array = get_children()
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
#super._init()
|
||||||
|
Log.pr("ProjectileLightning _init")
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
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()
|
||||||
|
|
||||||
|
emit_signal("on_spawned", self)
|
||||||
|
connect("body_entered", _on_body_entered)
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
for child in lightning:
|
||||||
|
child.goal_point = target_position
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
position += direction * speed * delta
|
||||||
|
|
||||||
|
func _on_lifetime_timeout():
|
||||||
|
super._on_lifetime_timeout()
|
||||||
1
player/weapons/projectile_lightning.gd.uid
Normal file
1
player/weapons/projectile_lightning.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cb7fwabidqyj8
|
||||||
|
|
@ -31,7 +31,7 @@ func _init() -> void:
|
||||||
add_child(fire_timer)
|
add_child(fire_timer)
|
||||||
fire_timer.one_shot = true
|
fire_timer.one_shot = true
|
||||||
|
|
||||||
projectile_scene = preload("res://assets/projectiles/basic_projectile.tscn")
|
projectile_scene = preload("res://assets/projectiles/projectile_lightning.tscn")
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
stats.connect("stats_updated", _on_stats_updated)
|
stats.connect("stats_updated", _on_stats_updated)
|
||||||
|
|
@ -40,16 +40,16 @@ func _ready():
|
||||||
# Initial update
|
# Initial update
|
||||||
_on_stats_updated()
|
_on_stats_updated()
|
||||||
|
|
||||||
func fire(direction: Vector2):
|
func fire(direction: Vector2, target_position: Vector2):
|
||||||
if !can_fire:
|
if !can_fire:
|
||||||
return
|
return
|
||||||
|
|
||||||
_spawn_projectile(global_position, direction)
|
_spawn_projectile(global_position, direction, target_position)
|
||||||
|
|
||||||
can_fire = false
|
can_fire = false
|
||||||
fire_timer.start(1.0 / stats.get_stat("fire_rate"))
|
fire_timer.start(1.0 / stats.get_stat("fire_rate"))
|
||||||
|
|
||||||
func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2):
|
func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target_position: Vector2):
|
||||||
# Get projectile quantity and spread from stats
|
# Get projectile quantity and spread from stats
|
||||||
var quantity = stats.get_stat("projectile_quantity")
|
var quantity = stats.get_stat("projectile_quantity")
|
||||||
var spread_angle = stats.get_stat("projectile_spread")
|
var spread_angle = stats.get_stat("projectile_spread")
|
||||||
|
|
@ -66,6 +66,7 @@ func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2):
|
||||||
for i in range(quantity):
|
for i in range(quantity):
|
||||||
var projectile = projectile_scene.instantiate()
|
var projectile = projectile_scene.instantiate()
|
||||||
projectile.global_position = spawn_position
|
projectile.global_position = spawn_position
|
||||||
|
projectile.target_position = target_position
|
||||||
|
|
||||||
# Calculate the direction with spread
|
# Calculate the direction with spread
|
||||||
var direction = spawn_direction
|
var direction = spawn_direction
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue