From 1a959fbc0c6e3c9ffd77ab08fabbd3705a81674a Mon Sep 17 00:00:00 2001 From: Dan Baker Date: Tue, 6 May 2025 09:30:44 +0100 Subject: [PATCH] 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. --- assets/projectiles/components/bolt.gd | 23 ++- assets/projectiles/projectile_lightning.tscn | 7 +- map/Map.tscn | 12 ++ player/scripts/player_combat.gd | 18 +- player/weapons/projectile_base.gd | 18 +- player/weapons/projectile_lightning.gd | 191 ++++++++++++++++++- player/weapons/ranged_weapon.gd | 4 +- 7 files changed, 232 insertions(+), 41 deletions(-) diff --git a/assets/projectiles/components/bolt.gd b/assets/projectiles/components/bolt.gd index 66a3e4f..bedfd61 100644 --- a/assets/projectiles/components/bolt.gd +++ b/assets/projectiles/components/bolt.gd @@ -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 \ No newline at end of file diff --git a/assets/projectiles/projectile_lightning.tscn b/assets/projectiles/projectile_lightning.tscn index 3882142..e94f6f2 100644 --- a/assets/projectiles/projectile_lightning.tscn +++ b/assets/projectiles/projectile_lightning.tscn @@ -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")] diff --git a/map/Map.tscn b/map/Map.tscn index e98271a..671f18d 100644 --- a/map/Map.tscn +++ b/map/Map.tscn @@ -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) diff --git a/player/scripts/player_combat.gd b/player/scripts/player_combat.gd index 65b9fc9..06b283f 100644 --- a/player/scripts/player_combat.gd +++ b/player/scripts/player_combat.gd @@ -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() diff --git a/player/weapons/projectile_base.gd b/player/weapons/projectile_base.gd index 887a8ec..6475c36 100644 --- a/player/weapons/projectile_base.gd +++ b/player/weapons/projectile_base.gd @@ -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() diff --git a/player/weapons/projectile_lightning.gd b/player/weapons/projectile_lightning.gd index 002f5ae..346a5b9 100644 --- a/player/weapons/projectile_lightning.gd +++ b/player/weapons/projectile_lightning.gd @@ -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() diff --git a/player/weapons/ranged_weapon.gd b/player/weapons/ranged_weapon.gd index 0eb5a13..3504213 100644 --- a/player/weapons/ranged_weapon.gd +++ b/player/weapons/ranged_weapon.gd @@ -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: