class_name EnemyBase extends CharacterBody2D @export var move_speed: float = 100 @export var max_health: float = 10.0 @export var default_contact_damage: float = 0.0 @export var target_distance: float = 6.0 @export var path_update_interval: float = 1.5 @export var xp_dropped: float = 5.0 @onready var target_cast: RayCast2D = $TargetCast @onready var animation_player: AnimationPlayer = $AnimationPlayer @onready var contact_damage_cd: Timer = $ContactDamageCD @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D @onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D @onready var shape_cast_2d: ShapeCast2D = $ShapeCast2D @onready var sprite_2d: Sprite2D = $Sprite2D var player: Player var enemy_name: String var target: Node2D var god_mode: bool = false var is_dead: bool = false var health: float var _path_update_timer: float = 0.0 func _ready() -> void: health = max_health enemy_name = _gen_name() shape_cast_2d.shape.radius = collision_shape_2d.shape.radius shape_cast_2d.enabled = false sprite_2d.material = sprite_2d.material.duplicate() _find_player() func _find_player(): if not player: player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER) target = player func _gen_name() -> String: return "Unnamed enemy" func _physics_process(delta: float) -> void: if not target: return do_movement(delta) check_contact_damage() func do_movement(delta: float) -> void: if not target: return if global_position.distance_to(target.global_position) < target_distance: return _path_update_timer -= delta + randf() if _has_direct_path(): shape_cast_2d.enabled = false _do_simple_movement() else: _do_nav_agent_movement() if velocity.x < 0: sprite_2d.flip_h = true else: sprite_2d.flip_h = false func _has_direct_path(): target_cast.target_position = to_local(target.global_position) target_cast.enabled = true return not target_cast.is_colliding() func _do_simple_movement(): var direction = global_position.direction_to(target.global_position) var distance = global_position.distance_to(target.global_position) if distance > 4: velocity = direction * move_speed move_and_slide() func _do_nav_agent_movement(): if _path_update_timer <= 0.0: _path_update_timer = path_update_interval nav_agent.target_position = target.global_position if nav_agent.is_navigation_finished(): return var next_point = nav_agent.get_next_path_position() var direction = (next_point - global_position).normalized() shape_cast_2d.target_position = to_local(next_point) shape_cast_2d.enabled = true if shape_cast_2d.is_colliding(): direction = direction.bounce(shape_cast_2d.get_collision_normal(0)).normalized() velocity = direction * move_speed move_and_slide() func check_contact_damage(): if default_contact_damage == 0.0: return if global_position.distance_to(target.global_position) > target_distance + 2: return deal_contact_damage() func deal_contact_damage(): if target.is_in_group("damagable"): if contact_damage_cd.is_stopped(): target.take_damage(default_contact_damage) contact_damage_cd.start() func take_damage(value: float, is_crit: bool = false): if god_mode: return health -= value var dm = preload("res://scenes/damage_numbers.tscn").instantiate() dm.damage_taken = value dm.critical_damage = is_crit animation_player.play("generic_anims/take_damage") add_child(dm) if health <= 0: die() func cheer(): target_distance = 2 if not animation_player.is_playing(): animation_player.play("generic_anims/cheer") func die(): if is_dead: return is_dead = true drop_xp_orb() target = null velocity = Vector2.ZERO animation_player.play("generic_anims/die") func drop_xp_orb() -> void: var orb: XPOrb = preload("res://scenes/xp_orb.tscn").instantiate() orb.value = xp_dropped orb.position = position get_parent().call_deferred("add_child", orb) func _on_animation_player_animation_finished(anim_name: StringName) -> void: if is_dead: queue_free()