# Enemy.gd # ───────────────────────────────────────────────────────────────────────────── # Gegner-KI – State Machine mit NavigationAgent3D # # Zustände: # IDLE → wartet bis Spieler in detection_range kommt # CHASING → läuft via NavMesh zum Spieler # ATTACKING → steht, dreht sich zum Spieler, greift in attack_speed-Intervallen an # DEAD → Kollision deaktiviert, Node wird nach kurzer Verzögerung entfernt # # Signale: # enemy_died(spawn_position, xp_reward) → World.gd → Respawn + XP # enemy_dropped_loot(loot, world_pos) → World.gd → Spieler → LootWindow # ───────────────────────────────────────────────────────────────────────────── extends CharacterBody3D # ═══════════════════════════════════════════════════════════════ # SIGNALE # ═══════════════════════════════════════════════════════════════ signal enemy_died(spawn_position: Vector3, xp_reward: int) signal enemy_dropped_loot(loot: Dictionary, world_pos: Vector3) # ═══════════════════════════════════════════════════════════════ # STATS # ═══════════════════════════════════════════════════════════════ @export var max_hp: int = 50 @export var min_damage: int = 3 @export var max_damage: int = 7 @export var attack_range: float = 2.0 @export var attack_speed: float = 2.0 # Sekunden zwischen Angriffen @export var move_speed: float = 3.0 @export var xp_reward: int = 20 @export var detection_range: float = 15.0 @export var loot_table: LootTable = null var current_hp: int var target = null # Spieler # ═══════════════════════════════════════════════════════════════ # ZUSTAND # ═══════════════════════════════════════════════════════════════ enum State { IDLE, CHASING, ATTACKING, DEAD } var state: State = State.IDLE var attack_cooldown: float = 0.0 var is_dead: bool = false const GRAVITY = 9.8 # ═══════════════════════════════════════════════════════════════ # NODE-REFERENZEN # ═══════════════════════════════════════════════════════════════ @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var health_label: Label3D = $HealthDisplay/Label3D # ═══════════════════════════════════════════════════════════════ # READY # ═══════════════════════════════════════════════════════════════ func _ready(): current_hp = max_hp _update_health_display() health_label.visible = false # NavigationAgent konfigurieren nav_agent.path_desired_distance = 0.5 nav_agent.target_desired_distance = attack_range * 0.9 # ═══════════════════════════════════════════════════════════════ # PHYSICS PROCESS # ═══════════════════════════════════════════════════════════════ func _physics_process(delta): if is_dead: return # Schwerkraft if not is_on_floor(): velocity.y -= GRAVITY * delta # Cooldown herunterzählen if attack_cooldown > 0: attack_cooldown -= delta # Kein Ziel → Idle if target == null or not is_instance_valid(target): state = State.IDLE velocity.x = 0 velocity.z = 0 move_and_slide() return var distance = global_position.distance_to(target.global_position) match state: State.IDLE: if distance <= detection_range: state = State.CHASING State.CHASING: if distance <= attack_range: state = State.ATTACKING velocity.x = 0 velocity.z = 0 else: _move_toward_target() State.ATTACKING: if distance > attack_range * 1.5: state = State.CHASING else: velocity.x = 0 velocity.z = 0 _face_target() if attack_cooldown <= 0: _perform_attack() move_and_slide() func _move_toward_target(): if target == null: return nav_agent.target_position = target.global_position if nav_agent.is_navigation_finished(): return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() direction.y = 0 velocity.x = direction.x * move_speed velocity.z = direction.z * move_speed _face_direction(direction) func _face_target(): if target == null: return var dir = (target.global_position - global_position) dir.y = 0 if dir.length() > 0.01: _face_direction(dir.normalized()) func _face_direction(dir: Vector3): if dir.length() > 0.01: rotation.y = atan2(dir.x, dir.z) # ═══════════════════════════════════════════════════════════════ # KAMPF # ═══════════════════════════════════════════════════════════════ func _perform_attack(): if target == null or not is_instance_valid(target): return var damage = randi_range(min_damage, max_damage) target.take_damage(damage) attack_cooldown = attack_speed print(name + " greift an: " + str(damage) + " Schaden") func take_damage(amount: int): if is_dead: return current_hp = clamp(current_hp - amount, 0, max_hp) _update_health_display() if current_hp <= 0: _die() # ═══════════════════════════════════════════════════════════════ # HP-ANZEIGE # ═══════════════════════════════════════════════════════════════ func show_health(): if health_label: health_label.visible = true func hide_health(): if health_label: health_label.visible = false func _update_health_display(): if health_label: health_label.text = str(current_hp) + " / " + str(max_hp) # ═══════════════════════════════════════════════════════════════ # TOD & LOOT # ═══════════════════════════════════════════════════════════════ func _die(): is_dead = true state = State.DEAD velocity = Vector3.ZERO print(name + " gestorben!") # Loot generieren if loot_table != null: var loot = loot_table.generate_loot() enemy_dropped_loot.emit(loot, global_position) # XP und Respawn-Signal enemy_died.emit(global_position, xp_reward) # Kollision deaktivieren und Node entfernen set_deferred("collision_layer", 0) set_deferred("collision_mask", 0) await get_tree().create_timer(1.5).timeout queue_free()