extends CharacterBody3D const SKELETON_WARRIOR: PackedScene = preload("res://assets/models/characters/Skeleton_Warrior.glb") const SKELETON_MAGE: PackedScene = preload("res://assets/models/characters/Skeleton_Mage.glb") @export var stats: EnemyStats var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") var spawn_scale: float = 1.0 var anim_player: AnimationPlayer = null var current_anim: String = "" var attack_lock_until: float = 0.0 var is_dying: bool = false func _ready() -> void: add_to_group("enemies") if is_in_group("boss"): BossData.register(self, stats, spawn_scale) BossData.set_stat(self, "spawn_position", global_position) _swap_model(SKELETON_MAGE, 1.3) else: EnemyData.register(self, stats, spawn_scale) EnemyData.set_stat(self, "spawn_position", global_position) EventBus.entity_died.connect(_on_entity_died) EventBus.attack_executed.connect(_on_attack_executed) call_deferred("_check_variant") call_deferred("_init_anim") func _init_anim() -> void: anim_player = get_node_or_null("Mesh/Model/AnimationPlayer") _play_anim("Idle") func _check_variant() -> void: if is_in_group("boss"): return if is_in_group("red_enemies") or is_in_group("invasion"): _swap_model(SKELETON_WARRIOR, 1.0) anim_player = get_node_or_null("Mesh/Model/AnimationPlayer") _play_anim("Idle") func _swap_model(new_scene: PackedScene, scale_factor: float = 1.0) -> void: var mesh: Node3D = get_node_or_null("Mesh") if not mesh: return var old: Node = mesh.get_node_or_null("Model") if old: old.queue_free() var new_model: Node3D = new_scene.instantiate() new_model.name = "Model" new_model.scale = Vector3(scale_factor, scale_factor, scale_factor) new_model.position = Vector3(0, -0.75, 0) new_model.rotation.y = PI mesh.add_child(new_model) func _exit_tree() -> void: if is_in_group("boss"): BossData.deregister(self) else: EnemyData.deregister(self) func _on_entity_died(entity: Node) -> void: if entity != self: return is_dying = true _play_anim("Death_A", false) get_tree().create_timer(1.0).timeout.connect(queue_free) func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void: if attacker != self: return _play_anim("1H_Melee_Attack_Chop", false) attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5 func _process(_delta: float) -> void: if is_dying: return var now: float = Time.get_ticks_msec() / 1000.0 if now < attack_lock_until: return if velocity.length() > 0.1: _play_anim("Running_A") else: _play_anim("Idle") func _play_anim(anim_name: String, loop: bool = true) -> void: if not anim_player: return if current_anim == anim_name: return if not anim_player.has_animation(anim_name): return var anim: Animation = anim_player.get_animation(anim_name) if anim: anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE anim_player.play(anim_name) current_anim = anim_name func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= gravity * delta move_and_slide()