From d0c2a7b3c8e4377cdaddb74bb926576d894b81b7 Mon Sep 17 00:00:00 2001 From: Dan Baker Date: Mon, 5 May 2025 16:42:51 +0100 Subject: [PATCH] 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. --- assets/projectiles/components/bolt.gd | 51 ++++++++++ assets/projectiles/components/bolt.gd.uid | 1 + assets/projectiles/components/bolt.tscn | 22 +++++ assets/projectiles/projectile_lightning.tscn | 25 +++++ player/scripts/player.gd | 8 +- player/scripts/player_combat.gd | 2 +- player/weapons/projectile.gd | 97 +------------------- player/weapons/projectile_base.gd | 95 +++++++++++++++++++ player/weapons/projectile_base.gd.uid | 1 + player/weapons/projectile_lightning.gd | 30 ++++++ player/weapons/projectile_lightning.gd.uid | 1 + player/weapons/ranged_weapon.gd | 9 +- 12 files changed, 239 insertions(+), 103 deletions(-) create mode 100644 assets/projectiles/components/bolt.gd create mode 100644 assets/projectiles/components/bolt.gd.uid create mode 100644 assets/projectiles/components/bolt.tscn create mode 100644 assets/projectiles/projectile_lightning.tscn create mode 100644 player/weapons/projectile_base.gd create mode 100644 player/weapons/projectile_base.gd.uid create mode 100644 player/weapons/projectile_lightning.gd create mode 100644 player/weapons/projectile_lightning.gd.uid diff --git a/assets/projectiles/components/bolt.gd b/assets/projectiles/components/bolt.gd new file mode 100644 index 0000000..66a3e4f --- /dev/null +++ b/assets/projectiles/components/bolt.gd @@ -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 diff --git a/assets/projectiles/components/bolt.gd.uid b/assets/projectiles/components/bolt.gd.uid new file mode 100644 index 0000000..8f4107c --- /dev/null +++ b/assets/projectiles/components/bolt.gd.uid @@ -0,0 +1 @@ +uid://bshl5y6qhgv2b diff --git a/assets/projectiles/components/bolt.tscn b/assets/projectiles/components/bolt.tscn new file mode 100644 index 0000000..406966d --- /dev/null +++ b/assets/projectiles/components/bolt.tscn @@ -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"] diff --git a/assets/projectiles/projectile_lightning.tscn b/assets/projectiles/projectile_lightning.tscn new file mode 100644 index 0000000..3882142 --- /dev/null +++ b/assets/projectiles/projectile_lightning.tscn @@ -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")] diff --git a/player/scripts/player.gd b/player/scripts/player.gd index e893280..f55189e 100644 --- a/player/scripts/player.gd +++ b/player/scripts/player.gd @@ -26,13 +26,13 @@ func _ready(): combat.animated_sprite = animated_sprite 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 # Size is now 1.0 + 0.5 = 1.5 # Add the multiplicative size modifier (1.5x multiplier) 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")) # Size is now 1.5 * 1.5 = 2.25 @@ -40,10 +40,10 @@ func _ready(): Log.pr("Adding another projectile size additive modifier", 2) var another_size_mod = ProjectileSizeAdditive.new() 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")) - weapon.add_modifier(FireRateAdditive.new()) + #weapon.add_modifier(FireRateAdditive.new()) func _physics_process(delta): diff --git a/player/scripts/player_combat.gd b/player/scripts/player_combat.gd index 21a25ae..65b9fc9 100644 --- a/player/scripts/player_combat.gd +++ b/player/scripts/player_combat.gd @@ -19,7 +19,7 @@ func process(_delta): var normalized_direction = direction.normalized() if Input.is_action_pressed("fire"): - player.weapon.fire(normalized_direction) + player.weapon.fire(normalized_direction, mouse_position) # Update animation #update_animation() diff --git a/player/weapons/projectile.gd b/player/weapons/projectile.gd index 62e9956..38bec04 100644 --- a/player/weapons/projectile.gd +++ b/player/weapons/projectile.gd @@ -1,27 +1,5 @@ -class_name Projectile 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 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 +class_name Projectile +extends ProjectileBase func _ready(): lifetime_timer = Timer.new() @@ -37,74 +15,5 @@ func _ready(): func _physics_process(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(): - destroy() + super._on_lifetime_timeout() diff --git a/player/weapons/projectile_base.gd b/player/weapons/projectile_base.gd new file mode 100644 index 0000000..887a8ec --- /dev/null +++ b/player/weapons/projectile_base.gd @@ -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() \ No newline at end of file diff --git a/player/weapons/projectile_base.gd.uid b/player/weapons/projectile_base.gd.uid new file mode 100644 index 0000000..061bc74 --- /dev/null +++ b/player/weapons/projectile_base.gd.uid @@ -0,0 +1 @@ +uid://c4bk8ac2ok2fx diff --git a/player/weapons/projectile_lightning.gd b/player/weapons/projectile_lightning.gd new file mode 100644 index 0000000..002f5ae --- /dev/null +++ b/player/weapons/projectile_lightning.gd @@ -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() diff --git a/player/weapons/projectile_lightning.gd.uid b/player/weapons/projectile_lightning.gd.uid new file mode 100644 index 0000000..f1df6fe --- /dev/null +++ b/player/weapons/projectile_lightning.gd.uid @@ -0,0 +1 @@ +uid://cb7fwabidqyj8 diff --git a/player/weapons/ranged_weapon.gd b/player/weapons/ranged_weapon.gd index 99ab71a..0eb5a13 100644 --- a/player/weapons/ranged_weapon.gd +++ b/player/weapons/ranged_weapon.gd @@ -31,7 +31,7 @@ func _init() -> void: add_child(fire_timer) 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(): stats.connect("stats_updated", _on_stats_updated) @@ -40,16 +40,16 @@ func _ready(): # Initial update _on_stats_updated() -func fire(direction: Vector2): +func fire(direction: Vector2, target_position: Vector2): if !can_fire: return - _spawn_projectile(global_position, direction) + _spawn_projectile(global_position, direction, target_position) can_fire = false 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 var quantity = stats.get_stat("projectile_quantity") 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): var projectile = projectile_scene.instantiate() projectile.global_position = spawn_position + projectile.target_position = target_position # Calculate the direction with spread var direction = spawn_direction