extends CharacterBody3D class_name Projectile enum State { INIT, TRAVEL, IMPACT, EFFECT, DESPAWN } enum Mode { FOLLOW, ## Follow target LOCATION, ## Go to target location SPAWN_ON_TARGET, ## Spawn on target location HITSCAN, ## Spawn on target location } enum Effect { ONE_HIT, ## Make damage on hit ## 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, } 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 } @export var mode : Mode = Mode.FOLLOW @export var effect : Effect = Effect.ONE_HIT @export var type : Type = Type.BASIC @export var speed : int ## Usefull when [enum Type] is not [constant BASIC][br][code]-1[/code] for no maximum @export var maxTargets : int = 1 ## 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 ## @export_group("Sounds - Animations") @export var impactScene : PackedScene @export var shootSFX : AudioStream @export var impactSFX : AudioStream var state : State = State.INIT var amount : float var target : PhysicsBody3D var vectorTarget : Vector3 var bodiesInRange : Array[Node3D] var collidingBodies : Array[Node3D] var affectedTarget : Array[Node3D] 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) func _physics_process(_delta: float) -> void: if state == State.INIT: if shootSFX: AudioManager.playSFX3D(shootSFX, global_position) state = State.TRAVEL return match state: State.TRAVEL when isOnTarget(): 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: state = State.EFFECT impactAnimations() applyEffects() State.EFFECT: return State.DESPAWN: queue_free.call_deferred() if shouldQueueFree(): state = State.DESPAWN func shouldQueueFree() -> bool: if !is_instance_valid(target): return mode == Mode.FOLLOW || maxTargets == 0 elif target is Tower: return not target.visible return maxTargets == 0 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 func isOnTarget() -> bool: match mode: Mode.LOCATION: return vectorTarget.distance_squared_to(global_position) < .4 Mode.FOLLOW: return collidingBodies.has(target) Mode.HITSCAN, Mode.SPAWN_ON_TARGET: return true _: return false func applyEffects() -> void: if not collidingBodies.is_empty() && isOnTarget(): resolveContacts() if state != State.TRAVEL: state = State.DESPAWN if effect == Effect.DAMAGE_OVER_TIME: dotTicks -= 1 if dotTicks > 0: state = State.EFFECT await get_tree().create_timer(tickInterval).timeout applyEffects.call_deferred() func resolveContacts() -> void: match type: Type.AOE: if maxTargets > 0: # No need to sort if we want to hit all targets collidingBodies.sort_custom(sortTargets) collidingBodies.map(resolveEffect) _ when collidingBodies.has(target): resolveEffect(target) if type == Type.BOUNCING: 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] func resolveEffect(body: Node3D) -> void: if body is GameTile || maxTargets == 0: return if type == Type.DISABLING && body.has_method("disable"): body.disable(amount) elif body.has_method("take_damage"): body.take_damage(amount) if effect == Effect.POISON: var poisonTicks : int = dotTicks - 1 while poisonTicks > 0 && is_instance_valid(body): poisonTicks -= 1 await get_tree().create_timer(tickInterval).timeout body.take_damage(amount) affectedTarget.append(body) maxTargets -= 1 func shoot(_target: Node3D, globalPos: Vector3) -> void: target = _target vectorTarget = globalPos 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) match mode: Mode.SPAWN_ON_TARGET: vectorTarget = targetPosition 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)