- Zauberstab als eigener Fernkampf-Skill (20m, magisch, exklusiv mit Autoattack) - Frostblitz mit 1.5s Castzeit und Castbar (mittig über Aktionsleiste) - Cast wird durch Bewegung, Springen oder Schaden unterbrochen - Holzstab als Magier-Startwaffe (+3 INT) - Frostblitz-Icon (SVG) - Skills klassenabhängig: Magier=Zauberstab+Frostblitz, Krieger/Schurke=Heavy Strike - Inventar: Drag & Drop zum Umordnen mit gelbem Highlight - Gegner aggrot sofort bei Schadenstreffer (nicht nur in Aggro-Range) - Inventar: swap_items/move_items Funktionen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
238 lines
7.1 KiB
GDScript
238 lines
7.1 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
|
|
|
|
@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()
|
|
|
|
# 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)
|
|
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:
|
|
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
|
|
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
|
|
_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
|
|
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():
|
|
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
|
|
# 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
|