TowerDefense/Projectiles/Projectile.gd
2025-09-14 22:58:52 +02:00

135 lines
3.3 KiB
GDScript

extends CharacterBody3D
class_name Projectile
enum MODE {
FOLLOW, ## Follow Entity
LOCATION, ## Go to entity location
HITSCAN, ## DANGER NOT implemented yet
}
enum TYPE { ## Types of projectiles
BASIC, ## One target
AOE, ## Multiple targets[br]work with [member damageArea]
PIERCING, ## Piercing through enemies[br]work with [member maxTarets] and [member damageArea]
BOUNCING, ## Bouncing over enemies[br]work with [member maxTarets] and [member damageArea]
DISABLING, ## Disable ally tower for [member damage] duration [br]Usable on [Boss] projectiles
}
@export var type : TYPE = TYPE.BASIC
@export var mode : MODE = MODE.FOLLOW
@export var speed : int
@export var maxTargets : int = 1
var amount : float
var target : PhysicsBody3D
var vectorTarget : Vector3
var bodiesInRange : Array[Node3D]
var affectedTarget : Array[Node3D]
func _physics_process(_delta: float) -> void:
if mode == MODE.LOCATION && vectorTarget.distance_squared_to(global_position) < .4:
resolveContact()
maxTargets = 0 # Ensure queue free in next if
if shouldQueueFree():
queue_free()
return
var globalPos : Vector3 = vectorTarget if vectorTarget else target.global_position
if target:
globalPos.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
look_at(globalPos)
velocity = global_position.direction_to(globalPos) * speed
move_and_slide()
func shouldQueueFree() -> bool:
if maxTargets < 1:
return true
if !is_instance_valid(target):
return mode == MODE.FOLLOW
elif target is Tower:
return not target.visible
return false
func onBodyEnteredDamageArea(body: Node3D) -> void:
if type != TYPE.BASIC && targetable(body):
addBodyInRange(body)
if type == TYPE.PIERCING:
resolveContact()
func onBodyCollideWithProjectile(body: Node3D) -> void:
if mode != MODE.LOCATION && (body == target || type == TYPE.PIERCING) && targetable(body):
addBodyInRange(body, true)
resolveContact()
func addBodyInRange(body: Node3D, pushFront: bool = false) -> void:
var idx : int = bodiesInRange.find(body)
if pushFront:
if idx != -1:
bodiesInRange.remove_at(idx)
bodiesInRange.push_front(body)
elif idx == -1:
bodiesInRange.push_back(body)
func targetable(body: Node3D) -> bool:
return not affectedTarget.has(body)
func resolveContact() -> void:
if bodiesInRange.is_empty():
return
resolveEffect(bodiesInRange[0])
match type:
TYPE.AOE:
for _body in bodiesInRange:
if is_instance_valid(_body):
resolveEffect(_body)
TYPE.BOUNCING:
target = null if bodiesInRange.is_empty() else bodiesInRange[0]
func resolveEffect(body : Node3D) -> void:
bodiesInRange.erase(body)
if affectedTarget.has(body) || body is GameTile:
return
if type == TYPE.DISABLING && body.has_method("disable"):
body.disable(amount)
elif body.has_method("take_damage"):
body.take_damage(amount)
affectedTarget.append(body)
maxTargets -= 1
func removeTarget(body: Node3D) -> void:
bodiesInRange.erase(body)
func shoot(_target: Node3D, globalPos: Vector3) -> void:
target = _target
match mode:
Projectile.MODE.HITSCAN:
globalPos = target.global_position
globalPos.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
Projectile.MODE.LOCATION:
vectorTarget = target.global_position
vectorTarget.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
EventBus.projectile_shooted.emit(self, globalPos)