2025-09-02 19:49:40 +02:00
|
|
|
extends CharacterBody3D
|
|
|
|
|
class_name Projectile
|
|
|
|
|
|
|
|
|
|
|
2025-09-21 13:08:20 +02:00
|
|
|
enum State { INIT, TRAVEL, IMPACT, EFFECT, DESPAWN }
|
2025-09-21 00:32:13 +02:00
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
enum Mode {
|
|
|
|
|
FOLLOW, ## Follow target
|
|
|
|
|
LOCATION, ## Go to target location
|
|
|
|
|
SPAWN_ON_TARGET, ## Spawn on target location
|
|
|
|
|
HITSCAN, ## Spawn on target location
|
2025-09-14 22:54:54 +02:00
|
|
|
}
|
2025-09-02 19:49:40 +02:00
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
enum Effect {
|
2025-09-20 19:58:50 +02:00
|
|
|
ONE_HIT, ## Make damage on hit
|
2025-09-21 00:32:13 +02:00
|
|
|
## Make damage every tick for specified duration[br]
|
|
|
|
|
## work with [member dotTicks] for number of tick
|
|
|
|
|
DAMAGE_OVER_TIME,
|
|
|
|
|
## Make damage over time on hitted target for the specified duration[br]
|
|
|
|
|
## work with [member dotTicks] for number of tick
|
|
|
|
|
POISON,
|
2025-09-20 19:58:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum Type { ## Types of projectiles
|
|
|
|
|
BASIC, ## One defined target
|
|
|
|
|
AOE, ## Multiple targets
|
|
|
|
|
BOUNCING, ## Bouncing over enemies[br]work with [member maxTargets]
|
|
|
|
|
DISABLING, ## Disable ally tower for [member amount] duration [br]Usable on [Boss] projectiles
|
2025-09-14 22:54:54 +02:00
|
|
|
}
|
2025-09-03 00:03:15 +02:00
|
|
|
|
|
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
@export var mode : Mode = Mode.FOLLOW
|
2025-09-21 00:32:13 +02:00
|
|
|
@export var effect : Effect = Effect.ONE_HIT
|
2025-09-20 19:58:50 +02:00
|
|
|
@export var type : Type = Type.BASIC
|
2025-09-14 22:54:54 +02:00
|
|
|
@export var speed : int
|
2025-09-21 00:32:13 +02:00
|
|
|
## Usefull when [enum Type] is not [constant BASIC][br][code]-1[/code] for no maximum
|
2025-09-14 22:54:54 +02:00
|
|
|
@export var maxTargets : int = 1
|
2025-09-21 00:32:13 +02:00
|
|
|
## The amount of ticks
|
|
|
|
|
## Used as time when [enum Effect] is [constant DAMAGE_OVER_TIME] or [constant POISON]
|
|
|
|
|
@export var dotTicks : int
|
|
|
|
|
@export var tickInterval : float ##
|
2025-09-14 01:31:18 +02:00
|
|
|
|
2025-09-21 13:08:20 +02:00
|
|
|
@export_group("Sounds - Animations")
|
|
|
|
|
@export var impactScene : PackedScene
|
|
|
|
|
@export var shootSFX : AudioStream
|
|
|
|
|
@export var impactSFX : AudioStream
|
|
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
var state : State = State.INIT
|
2025-09-14 22:54:54 +02:00
|
|
|
var amount : float
|
2025-09-02 19:49:40 +02:00
|
|
|
var target : PhysicsBody3D
|
|
|
|
|
var vectorTarget : Vector3
|
2025-09-14 22:54:54 +02:00
|
|
|
var bodiesInRange : Array[Node3D]
|
2025-09-15 19:45:59 +02:00
|
|
|
var collidingBodies : Array[Node3D]
|
2025-09-14 22:54:54 +02:00
|
|
|
var affectedTarget : Array[Node3D]
|
2025-09-02 19:49:40 +02:00
|
|
|
|
2025-09-03 00:03:15 +02:00
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
func _ready() -> void:
|
|
|
|
|
$HitBox.body_entered.connect(collidingBodies.append)
|
|
|
|
|
$HitBox.body_exited.connect(collidingBodies.erase)
|
|
|
|
|
$EffectArea.body_entered.connect(bodiesInRange.append)
|
|
|
|
|
$EffectArea.body_exited.connect(bodiesInRange.erase)
|
|
|
|
|
|
|
|
|
|
|
2025-09-03 03:44:44 +02:00
|
|
|
func _physics_process(_delta: float) -> void:
|
2025-09-21 00:32:13 +02:00
|
|
|
if state == State.INIT:
|
2025-09-21 13:08:20 +02:00
|
|
|
if shootSFX:
|
|
|
|
|
AudioManager.playSFX3D(shootSFX, global_position)
|
2025-09-21 00:32:13 +02:00
|
|
|
state = State.TRAVEL
|
2025-09-20 19:58:50 +02:00
|
|
|
return
|
2025-09-14 22:54:54 +02:00
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
match state:
|
|
|
|
|
State.TRAVEL when isOnTarget():
|
2025-09-21 13:08:20 +02:00
|
|
|
state = State.IMPACT
|
|
|
|
|
return
|
|
|
|
|
State.TRAVEL:
|
|
|
|
|
if mode == Mode.FOLLOW:
|
|
|
|
|
var globalPos : Vector3 = target.global_position
|
|
|
|
|
globalPos.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
|
|
|
|
|
look_at(globalPos)
|
|
|
|
|
velocity = global_position.direction_to(globalPos) * speed
|
|
|
|
|
move_and_slide()
|
|
|
|
|
State.IMPACT:
|
2025-09-21 00:32:13 +02:00
|
|
|
state = State.EFFECT
|
2025-09-21 13:08:20 +02:00
|
|
|
impactAnimations()
|
2025-09-21 00:32:13 +02:00
|
|
|
applyEffects()
|
|
|
|
|
State.EFFECT: return
|
|
|
|
|
State.DESPAWN:
|
|
|
|
|
queue_free.call_deferred()
|
2025-09-15 19:45:59 +02:00
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
if shouldQueueFree():
|
2025-09-21 00:32:13 +02:00
|
|
|
state = State.DESPAWN
|
|
|
|
|
|
2025-09-02 19:49:40 +02:00
|
|
|
|
2025-09-14 01:31:18 +02:00
|
|
|
func shouldQueueFree() -> bool:
|
2025-09-21 00:32:13 +02:00
|
|
|
if !is_instance_valid(target):
|
|
|
|
|
return mode == Mode.FOLLOW || maxTargets == 0
|
2025-09-14 22:54:54 +02:00
|
|
|
elif target is Tower:
|
|
|
|
|
return not target.visible
|
2025-09-14 01:31:18 +02:00
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
return maxTargets == 0
|
2025-09-14 01:31:18 +02:00
|
|
|
|
|
|
|
|
|
2025-09-21 13:08:20 +02:00
|
|
|
func impactAnimations() -> void:
|
|
|
|
|
if impactSFX:
|
|
|
|
|
AudioManager.playSFX3D(impactSFX, global_position)
|
|
|
|
|
if impactScene:
|
|
|
|
|
var impact : GPUParticles3D = impactScene.instantiate()
|
|
|
|
|
impact.finished.connect(impact.queue_free)
|
|
|
|
|
get_tree().root.add_child(impact)
|
|
|
|
|
impact.emitting = false
|
|
|
|
|
impact.one_shot = true
|
|
|
|
|
impact.emitting = true
|
|
|
|
|
|
|
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
func isOnTarget() -> bool:
|
|
|
|
|
match mode:
|
2025-09-21 00:32:13 +02:00
|
|
|
Mode.LOCATION: return vectorTarget.distance_squared_to(global_position) < .4
|
2025-09-20 19:58:50 +02:00
|
|
|
Mode.FOLLOW: return collidingBodies.has(target)
|
2025-09-21 00:32:13 +02:00
|
|
|
Mode.HITSCAN, Mode.SPAWN_ON_TARGET: return true
|
2025-09-20 19:58:50 +02:00
|
|
|
_: return false
|
2025-09-14 01:31:18 +02:00
|
|
|
|
|
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
func applyEffects() -> void:
|
|
|
|
|
if not collidingBodies.is_empty() && isOnTarget():
|
|
|
|
|
resolveContacts()
|
|
|
|
|
|
2025-09-21 13:08:20 +02:00
|
|
|
if state != State.TRAVEL:
|
|
|
|
|
state = State.DESPAWN
|
2025-09-21 00:32:13 +02:00
|
|
|
if effect == Effect.DAMAGE_OVER_TIME:
|
|
|
|
|
dotTicks -= 1
|
|
|
|
|
if dotTicks > 0:
|
|
|
|
|
state = State.EFFECT
|
|
|
|
|
await get_tree().create_timer(tickInterval).timeout
|
|
|
|
|
applyEffects.call_deferred()
|
|
|
|
|
|
2025-09-03 00:03:15 +02:00
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
func resolveContacts() -> void:
|
2025-09-14 22:54:54 +02:00
|
|
|
match type:
|
2025-09-20 19:58:50 +02:00
|
|
|
Type.AOE:
|
2025-09-21 00:32:13 +02:00
|
|
|
if maxTargets > 0: # No need to sort if we want to hit all targets
|
|
|
|
|
collidingBodies.sort_custom(sortTargets)
|
2025-09-20 19:58:50 +02:00
|
|
|
collidingBodies.map(resolveEffect)
|
|
|
|
|
_ when collidingBodies.has(target):
|
|
|
|
|
resolveEffect(target)
|
|
|
|
|
if type == Type.BOUNCING:
|
2025-09-21 00:32:13 +02:00
|
|
|
target = chooseNextTarget()
|
|
|
|
|
state = State.TRAVEL
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func sortTargets(body1: Node3D, body2: Node3D) -> bool:
|
|
|
|
|
return vectorTarget.distance_to(body1.global_position) < vectorTarget.distance_to(body2.global_position)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func chooseNextTarget() -> Node3D:
|
|
|
|
|
var bodies = bodiesInRange.filter(func(body): return not affectedTarget.has(body))
|
|
|
|
|
if bodies.is_empty():
|
|
|
|
|
return null
|
|
|
|
|
bodies.sort_custom(sortTargets)
|
|
|
|
|
return bodies[0]
|
2025-09-03 00:03:15 +02:00
|
|
|
|
|
|
|
|
|
2025-09-21 13:08:20 +02:00
|
|
|
func resolveEffect(body: Node3D) -> void:
|
2025-09-21 00:32:13 +02:00
|
|
|
if body is GameTile || maxTargets == 0:
|
2025-09-14 22:54:54 +02:00
|
|
|
return
|
2025-09-14 01:31:18 +02:00
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
if type == Type.DISABLING && body.has_method("disable"):
|
2025-09-14 22:54:54 +02:00
|
|
|
body.disable(amount)
|
|
|
|
|
elif body.has_method("take_damage"):
|
|
|
|
|
body.take_damage(amount)
|
2025-09-14 01:31:18 +02:00
|
|
|
|
2025-09-21 00:32:13 +02:00
|
|
|
if effect == Effect.POISON:
|
2025-09-21 13:08:20 +02:00
|
|
|
var poisonTicks : int = dotTicks - 1
|
|
|
|
|
while poisonTicks > 0 && is_instance_valid(body):
|
|
|
|
|
poisonTicks -= 1
|
2025-09-21 00:32:13 +02:00
|
|
|
await get_tree().create_timer(tickInterval).timeout
|
|
|
|
|
body.take_damage(amount)
|
|
|
|
|
|
2025-09-14 22:54:54 +02:00
|
|
|
affectedTarget.append(body)
|
|
|
|
|
maxTargets -= 1
|
2025-09-02 19:49:40 +02:00
|
|
|
|
|
|
|
|
|
2025-09-14 22:54:54 +02:00
|
|
|
func shoot(_target: Node3D, globalPos: Vector3) -> void:
|
2025-09-02 19:49:40 +02:00
|
|
|
target = _target
|
2025-09-21 00:32:13 +02:00
|
|
|
vectorTarget = globalPos
|
2025-09-02 19:49:40 +02:00
|
|
|
|
2025-09-20 19:58:50 +02:00
|
|
|
var transform3D : Transform3D = Transform3D()
|
|
|
|
|
transform3D.origin = globalPos
|
|
|
|
|
|
|
|
|
|
var targetPosition : Vector3 = target.global_position
|
|
|
|
|
targetPosition.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
|
|
|
|
|
transform3D = transform3D.looking_at(targetPosition)
|
2025-09-14 22:54:54 +02:00
|
|
|
match mode:
|
2025-09-20 19:58:50 +02:00
|
|
|
Mode.SPAWN_ON_TARGET:
|
2025-09-21 00:32:13 +02:00
|
|
|
vectorTarget = targetPosition
|
2025-09-20 19:58:50 +02:00
|
|
|
transform3D.origin = targetPosition
|
|
|
|
|
transform3D = transform3D.looking_at(globalPos)
|
|
|
|
|
Mode.LOCATION:
|
|
|
|
|
vectorTarget = targetPosition
|
|
|
|
|
velocity = transform3D.origin.direction_to(vectorTarget) * speed
|
|
|
|
|
|
|
|
|
|
EventBus.projectile_shooted.emit(self, transform3D)
|