extends Node3D class_name Projectile var velocity: Vector3 = Vector3.ZERO var damage: float = 10.0 var max_range: float = 200.0 var distance_traveled: float = 0.0 var owner_player: CharacterBody3D var falloff_start: float = 10.0 var min_damage: float = 5.0 var drop_gravity: float = 0.0 # For arc projectiles (nailgun might have slight drop, plasma none) var impact_type: String = "bullet" var logical_source = null var _previous_position: Vector3 func _ready() -> void: if logical_source != null: _previous_position = logical_source else: _previous_position = global_position # Automatically destroy after 5 seconds to prevent leaks var t = get_tree().create_timer(5.0) t.timeout.connect(destroy) func _physics_process(delta: float) -> void: # Apply gravity drop if any velocity.y -= drop_gravity * delta var movement = velocity * delta global_position += movement distance_traveled += movement.length() if distance_traveled > max_range: destroy() return _check_collision() _previous_position = global_position func _check_collision() -> void: var space_state = get_world_3d().direct_space_state var query = PhysicsRayQueryParameters3D.create(_previous_position, global_position) if owner_player: query.exclude = [owner_player.get_rid()] var result = space_state.intersect_ray(query) if result: _on_hit(result) func _on_hit(result: Dictionary) -> void: var hit_dist = distance_traveled - (_previous_position.distance_to(result.position)) var final_damage = damage if hit_dist > falloff_start and falloff_start > 0.0: var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0) final_damage = lerpf(damage, min_damage, falloff_factor) if result.collider.has_method("take_damage"): if owner_player and owner_player.multiplayer.is_server(): result.collider.take_damage(final_damage, result.position, owner_player) elif result.collider is StaticBody3D or result.collider is CSGShape3D: var size = 0.4 if impact_type == "plasma" else 0.15 ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size) destroy() func destroy() -> void: queue_free()