extends CharacterBody3D class_name Projectile enum State { INIT, TRAVEL, 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 ## 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: state = State.TRAVEL return match state: State.TRAVEL when isOnTarget(): state = State.EFFECT applyEffects() return State.EFFECT: return State.DESPAWN: queue_free.call_deferred() return if shouldQueueFree(): state = State.DESPAWN if state != State.TRAVEL: return 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() 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 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 effect == Effect.DAMAGE_OVER_TIME: dotTicks -= 1 state = State.DESPAWN 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: dotTicks -= 1 while dotTicks > 0 && is_instance_valid(body): dotTicks -= 1 await get_tree().create_timer(tickInterval).timeout body.take_damage(amount) affectedTarget.append(body) maxTargets -= 1 state = State.DESPAWN 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)