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)