DungeonCrawler/enemy.gd
Andre 3bdd0780c5 Mixamo Warrior-Modell mit Animationen, Zauberstab-Icon und Magier-Skills
- Kenney-Modelle durch Mixamo Warrior (warrior.fbx) für Player und Enemy ersetzt
- Animations-System: Lädt Walking, Strafe, Jump, Autoattack, Heavy Strike, Dying aus separaten FBX-Dateien
- Player: Bewegungsanimationen (walk/strafe/jump) + Kampfanimationen (autoattack/heavy_strike/die)
- Enemy: Walk-Animation für Patrol/Chase, Autoattack-Animation, Death-Animation mit Verzögerung
- Zauberstab-Icon (wand_icon.svg) erstellt und in Magier-Skills verknüpft
- Frostblitz und Zauberstab als klassenspezifische Magier-Skills dokumentiert
- Castbar-System, Gegner-Aggro bei Schaden und Drag&Drop in Dokumentation ergänzt
- Enemy patrouilliert jetzt auch ohne Spieler-Referenz

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:44:31 +01:00

339 lines
11 KiB
GDScript

# 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