DungeonCrawler/enemy.gd
Andre 394b3a89b8 Souls/RPG-Hybrid Bewegungssystem, Kamera, Animationen & Dokumentation
Bewegung:
- Souls-Modus (kein Ziel): Charakter dreht sich zur Laufrichtung relativ zu
  camera_pivot.world_yaw; bei RMB gehalten → Strafe statt Drehung
- Walk-Toggle (NumLock): RPG-Strafe-Modus mit langsamerer Geschwindigkeit
- Lock-On (Ziel markiert): Spieler dreht sich smooth zum Gegner, WASD = Strafe
- Ausweichrolle (Shift): rollt in Eingaberichtung (world_yaw-relativ im Souls-Modus)
- Sofort-180°-Snap statt animierter Drehung bei >150° Winkelunterschied

Kamera (camera_pivot.gd):
- world_yaw: absolute Weltausrichtung, unabhängig von Spielerrotation (kein Feedback-Loop)
- LMB gehalten: Kamera orbitet, Spieler dreht sich nicht
- RMB gehalten: Spieler + Kamera drehen sich gemeinsam
- Soft Lock-On: camera_pivot dreht Spieler smooth zum Ziel

Animationen:
- Neue FBX-Animationen: Quick Roll, Running Jump, Walking Jump,
  Running Strafe L/R, Running Turn 180
- Animationen im Souls-Modus: immer "run" vorwärts; S = walk_back
- Root-Motion-Strip: XZ-Bewegung auf Knochen-Tracks wird genullt

Welt:
- Boden-Shader: Schachbrettmuster in World-Space (INV_VIEW_MATRIX)
- ProceduralSkyMaterial + WorldEnvironment per Code
- Alte assets/animations und assets/models durch Warrior+Animation ersetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 22:26:10 +01:00

205 lines
8.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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()