# Enemy.gd # Steuert den Gegner: KI-Bewegung zum Spieler, Angriff, HP, Zielanzeige extends CharacterBody3D signal enemy_died(spawn_position: Vector3, xp_reward: int) signal enemy_dropped_loot(loot: Dictionary, world_position: Vector3) const SPEED = 3.0 const PATROL_SPEED = 1.5 const GRAVITY = 9.8 const ATTACK_RANGE = 1.5 const ATTACK_COOLDOWN = 2.0 const AGGRO_RANGE = 8.0 # Entfernung ab der der Gegner angreift const PATROL_RADIUS = 5.0 # Radius um Spawn-Position für Patrol const PATROL_WAIT_TIME = 2.0 # Wartezeit am Patrol-Punkt # Level-Differenz Konstanten const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation enum State { PATROL, CHASE, ATTACK } # Stats-System @export var level: int = 1 @export var base_strength: int = 8 @export var base_stamina: int = 10 @export var base_armor: int = 5 # Rüstung reduziert Nahkampfschaden # Berechnete Stats var strength: int = 8 var stamina: int = 10 var armor: int = 5 var max_hp: int = 100 var current_hp: int = 100 var attack_damage: int = 5 # XP-Belohnung (skaliert mit Level) var xp_reward: int = 25 # Loot-System @export var loot_table: LootTable var target = null # Spieler-Referenz (wird von World gesetzt) var can_attack = true var spawn_position: Vector3 # Ursprüngliche Spawn-Position var current_state = State.PATROL var patrol_target: Vector3 # Aktuelles Patrol-Ziel var is_waiting = false # Ob Gegner am Patrol-Punkt wartet # Animation System const ANIMATION_FILES = { "walk": "res://assets/animations/Walking.fbx", "autoattack": "res://assets/animations/Autoattack.fbx", "die": "res://assets/animations/Dying Backwards.fbx", } var anim_player: AnimationPlayer = null var current_anim: String = "" @onready var health_label = $HealthLabel func _ready(): _calculate_stats() current_hp = max_hp health_label.visible = false _update_label() spawn_position = global_position _pick_new_patrol_target() _setup_animations() # Animationen laden func _setup_animations(): # Debug: Alle Kinder ausgeben um den Modell-Node zu finden print("Enemy Kinder: ") for child in get_children(): print(" - ", child.name, " (", child.get_class(), ")") var model = get_node_or_null("EnemyModel") if model == null: print("Enemy: EnemyModel nicht gefunden!") return anim_player = _find_node_by_class(model, "AnimationPlayer") if anim_player == null: anim_player = AnimationPlayer.new() anim_player.name = "AnimationPlayer" model.add_child(anim_player) var lib: AnimationLibrary if anim_player.has_animation_library(""): lib = anim_player.get_animation_library("") else: lib = AnimationLibrary.new() anim_player.add_animation_library("", lib) for anim_id in ANIMATION_FILES: var scene = load(ANIMATION_FILES[anim_id]) as PackedScene if scene == null: print("Enemy: Animation nicht gefunden: ", ANIMATION_FILES[anim_id]) continue var instance = scene.instantiate() var source_ap = _find_node_by_class(instance, "AnimationPlayer") if source_ap: var names = source_ap.get_animation_list() if names.size() > 0: var anim = source_ap.get_animation(names[0]) if lib.has_animation(anim_id): lib.remove_animation(anim_id) lib.add_animation(anim_id, anim) print("Enemy: Animation geladen: ", anim_id) else: print("Enemy: Kein AnimationPlayer in ", ANIMATION_FILES[anim_id]) instance.queue_free() print("Enemy: Verfügbare Animationen: ", anim_player.get_animation_list()) # Debug: Modell-Hierarchie ausgeben _print_tree(model, 0) func _print_tree(node: Node, depth: int): print(" ".repeat(depth), node.name, " (", node.get_class(), ")") for child in node.get_children(): _print_tree(child, depth + 1) func _find_node_by_class(node: Node, class_name_str: String) -> Node: for child in node.get_children(): if child.get_class() == class_name_str: return child var result = _find_node_by_class(child, class_name_str) if result: return result return null func _play_anim(anim_name: String): if anim_player == null: print("Enemy: anim_player ist null!") return if anim_name != current_anim: current_anim = anim_name if anim_name != "" and anim_player.has_animation(anim_name): anim_player.play(anim_name) print("Enemy: Spiele Animation: ", anim_name) # Debug: Track-Pfade und root_node ausgeben var anim = anim_player.get_animation(anim_name) print(" AnimPlayer root_node: ", anim_player.root_node) print(" AnimPlayer absoluter Pfad: ", anim_player.get_path()) for t in range(mini(3, anim.get_track_count())): print(" Track ", t, ": ", anim.track_get_path(t)) elif anim_name != "": print("Enemy: Animation nicht gefunden: ", anim_name, " | Verfügbar: ", anim_player.get_animation_list()) else: anim_player.stop() # Stats basierend auf Level berechnen func _calculate_stats(): var levels_gained = level - 1 strength = base_strength + levels_gained * 2 stamina = base_stamina + levels_gained * 3 armor = base_armor + levels_gained * 2 # HP = Stamina * 10 max_hp = stamina * 10 # Schaden = Stärke / 2 attack_damage = int(strength * 0.5) + 2 # XP = 25 * Level xp_reward = 25 * level print("Enemy Stats (Lv", level, ") - STR:", strength, " STA:", stamina, " ARM:", armor, " HP:", max_hp, " DMG:", attack_damage) # Schaden mit Rüstung und Level-Differenz berechnen func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int: var damage = float(raw_damage) # Rüstung reduziert nur Nahkampfschaden if is_melee: # Rüstungsreduktion: armor / (armor + 50) = Prozent Reduktion # Bei 5 Rüstung: 5/55 = ~9% Reduktion # Bei 20 Rüstung: 20/70 = ~29% Reduktion var armor_reduction = float(armor) / (float(armor) + 50.0) damage = damage * (1.0 - armor_reduction) # Level-Differenz Modifikator var level_diff = attacker_level - level var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD) damage = damage * (1.0 + level_mod) return maxi(1, int(damage)) # Mindestens 1 Schaden # HP-Label Text aktualisieren func _update_label(): health_label.text = "Lv" + str(level) + " " + str(current_hp) + "/" + str(max_hp) # HP-Label anzeigen (wenn Gegner markiert wird) func show_health(): health_label.visible = true # HP-Label verstecken (wenn Markierung aufgehoben wird) func hide_health(): health_label.visible = false # Schaden nehmen und Label aktualisieren func take_damage(amount): current_hp -= amount _update_label() # Aggro bei Schaden — sofort angreifen if current_state == State.PATROL: current_state = State.CHASE is_waiting = false print("Gegner wurde angegriffen und verfolgt den Spieler!") if current_hp <= 0: die() # Schaden mit vollem Schadenssystem (Rüstung, Level-Differenz) func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true): var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee) print("Eingehender Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)") take_damage(final_damage) # Gegner aus der Szene entfernen func die(): print("Gegner besiegt! +", xp_reward, " XP") # XP an Spieler geben if target and target.has_method("gain_xp"): target.gain_xp(xp_reward) # Loot generieren und droppen _drop_loot() enemy_died.emit(spawn_position, xp_reward) # Death-Animation abspielen, dann entfernen if anim_player and anim_player.has_animation("die"): _play_anim("die") # Kollision deaktivieren damit der Gegner nicht mehr im Weg ist set_physics_process(false) $CollisionShape3D.set_deferred("disabled", true) await anim_player.animation_finished queue_free() # Loot generieren basierend auf LootTable func _drop_loot(): if loot_table == null: # Standard-Gold-Drop wenn keine LootTable zugewiesen var gold = randi_range(1, 3) * level var loot = {"gold": gold, "items": []} enemy_dropped_loot.emit(loot, global_position) return var loot = loot_table.generate_loot() # Gold mit Level skalieren loot["gold"] = loot["gold"] * level enemy_dropped_loot.emit(loot, global_position) func _physics_process(delta): if not is_on_floor(): velocity.y -= GRAVITY * delta if target == null: # Ohne Spieler-Referenz nur patrouillieren _do_patrol() move_and_slide() return # Prüfe Distanz zum Spieler für Aggro var distance_to_player = global_position.distance_to(target.global_position) # State-Wechsel basierend auf Distanz match current_state: State.PATROL: if distance_to_player <= AGGRO_RANGE: current_state = State.CHASE print("Gegner hat Spieler entdeckt!") else: _do_patrol() State.CHASE: if distance_to_player <= ATTACK_RANGE: current_state = State.ATTACK else: _chase_player() State.ATTACK: if distance_to_player > ATTACK_RANGE: current_state = State.CHASE else: velocity.x = 0 velocity.z = 0 _play_anim("") # Idle beim Angriff if can_attack: _attack() move_and_slide() # Neues Patrol-Ziel in der Nähe der Spawn-Position wählen func _pick_new_patrol_target(): var angle = randf() * TAU # Zufälliger Winkel var distance = randf_range(2.0, PATROL_RADIUS) patrol_target = spawn_position + Vector3(cos(angle) * distance, 0, sin(angle) * distance) # Patrol-Verhalten: Zufällig herumlaufen func _do_patrol(): if is_waiting: return var distance_to_patrol = global_position.distance_to(patrol_target) if distance_to_patrol <= 0.5: # Am Ziel angekommen, warten und neues Ziel wählen velocity.x = 0 velocity.z = 0 _play_anim("") _wait_at_patrol_point() else: # Zum Patrol-Ziel laufen var direction = (patrol_target - global_position) direction.y = 0 direction = direction.normalized() velocity.x = direction.x * PATROL_SPEED velocity.z = direction.z * PATROL_SPEED _play_anim("walk") look_at(Vector3(patrol_target.x, global_position.y, patrol_target.z)) # Am Patrol-Punkt warten func _wait_at_patrol_point(): is_waiting = true await get_tree().create_timer(PATROL_WAIT_TIME).timeout is_waiting = false _pick_new_patrol_target() # Spieler verfolgen func _chase_player(): _play_anim("walk") var direction = (target.global_position - global_position) direction.y = 0 direction = direction.normalized() velocity.x = direction.x * SPEED velocity.z = direction.z * SPEED look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z)) # Angriff mit Cooldown func _attack(): can_attack = false _play_anim("autoattack") # Gegner verwendet auch das Schadenssystem mit Level-Differenz if target.has_method("take_damage_from"): target.take_damage_from(attack_damage, level, true) else: target.take_damage(attack_damage) print("Gegner (Lv", level, ") greift an: ", attack_damage, " Schaden") await get_tree().create_timer(ATTACK_COOLDOWN).timeout can_attack = true