DungeonCrawler/player.gd
Andre 9ed18e034c Waffen-Modell an Spielerhand via BoneAttachment3D
Schwert (medieval_sword.glb) wird zur Laufzeit an mixamorig_RightHand
gehängt. Modell erscheint/verschwindet beim Ausrüsten/Ablegen.
Equipment-Ressource um model_scene Property erweitert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 01:25:19 +01:00

1077 lines
44 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.

# Player.gd
# ─────────────────────────────────────────────────────────────────────────────
# Hauptskript des Spielers RPG/Souls-Hybrid
#
# Bewegungssystem:
# • Kein Ziel + kein Walk → Souls-Modus: Charakter dreht sich zur Laufrichtung
# (relativ zu camera_pivot.world_yaw), Animation = "run"
# RMB gehalten: A/D = Strafe statt Drehung
# • Kein Ziel + Walk (NumLock) → RPG-Modus: Strafe-Animationen, langsame Geschwindigkeit
# • Ziel markiert → Lock-On: Kamera dreht Spieler zum Ziel,
# WASD = Strafe/Rückwärts relativ zur Spielerausrichtung
#
# Kamera-Referenz:
# dir3 (Bewegungsvektor) nutzt camera_pivot.world_yaw im Souls-Modus, damit sich
# der Vektor NICHT mit der Spielerrotation dreht (kein Feedback-Loop).
#
# Kampf:
# Autoattack, Heavy Strike (Krieger), Quick Strike (Schurke), Frostbolt (Magier)
# Global Cooldown, Skill Cooldowns, Cast-System
#
# Animationen:
# Werden zur Laufzeit aus FBX-Dateien geladen (_load_anim_from_fbx).
# Root Motion wird automatisch entfernt (_strip_root_motion).
# ─────────────────────────────────────────────────────────────────────────────
extends CharacterBody3D
# ═══════════════════════════════════════════════════════════════
# KONSTANTEN
# ═══════════════════════════════════════════════════════════════
const SPEED = 5.0
const SPRINT_SPEED = 9.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
# Scenes für UI-Panels (werden zur Laufzeit erstellt)
const LOOT_WINDOW_SCENE = preload("res://loot_window.tscn")
const INVENTORY_PANEL_SCENE = preload("res://inventory_panel.tscn")
const SKILL_PANEL_SCENE = preload("res://skill_panel.tscn")
const CHARACTER_PANEL_SCENE = preload("res://character_panel.tscn")
# Animations-FBX Pfade
const ANIM_IDLE = "res://assets/Warrior+Animation/idle.fbx"
const ANIM_WALK = "res://assets/Warrior+Animation/walking.fbx"
const ANIM_RUN = "res://assets/Warrior+Animation/running.fbx"
const ANIM_JUMP = "res://assets/Warrior+Animation/jump.fbx"
const ANIM_AUTOATTACK = "res://assets/Warrior+Animation/Autoattack.fbx"
const ANIM_HEAVY_STRIKE = "res://assets/Warrior+Animation/Heavy Strike.fbx"
const ANIM_DEATH = "res://assets/Warrior+Animation/Dying Backwards.fbx"
const ANIM_WALK_BACK = "res://assets/Warrior+Animation/Walking Backwards.fbx"
const ANIM_STRAFE_LEFT = "res://assets/Warrior+Animation/left strafe walking.fbx"
const ANIM_STRAFE_RIGHT = "res://assets/Warrior+Animation/right strafe walking.fbx"
const ANIM_RUN_STRAFE_LEFT = "res://assets/Warrior+Animation/Running Left Strafe.fbx"
const ANIM_RUN_STRAFE_RIGHT = "res://assets/Warrior+Animation/Running Right Strafe run.fbx"
const ANIM_WALK_JUMP = "res://assets/Warrior+Animation/Walking Jump.fbx"
const ANIM_RUN_JUMP = "res://assets/Warrior+Animation/Running Jump.fbx"
const ANIM_ROLL = "res://assets/Warrior+Animation/Quick Roll To Run.fbx"
const ANIM_TURN_180 = "res://assets/Warrior+Animation/Running Turn 180.fbx"
# ═══════════════════════════════════════════════════════════════
# CHARAKTER-STATS
# ═══════════════════════════════════════════════════════════════
var character_class: CharacterClass = null
var level: int = 1
var current_xp: int = 0
var xp_to_next_level: int = 100
# Basis-Stats (aus Klasse + Items berechnet)
var strength: int = 10
var agility: int = 10
var intelligence: int = 10
var stamina: int = 10
var armor: int = 0
# HP und Ressource
var max_hp: int = 100
var current_hp: int = 100
var max_resource: int = 0
var current_resource: int = 0
# Mana-Aliases (für Consumable-Kompatibilität)
var max_mana: int:
get: return max_resource
var current_mana: int:
get: return current_resource
# ═══════════════════════════════════════════════════════════════
# EQUIPMENT
# ═══════════════════════════════════════════════════════════════
var equipment: Dictionary = {
Equipment.Slot.HEAD: null,
Equipment.Slot.CHEST: null,
Equipment.Slot.HANDS: null,
Equipment.Slot.LEGS: null,
Equipment.Slot.FEET: null,
Equipment.Slot.WEAPON: null,
Equipment.Slot.OFFHAND: null,
}
# ═══════════════════════════════════════════════════════════════
# INVENTAR
# ═══════════════════════════════════════════════════════════════
var inventory: Inventory = Inventory.new()
# ═══════════════════════════════════════════════════════════════
# SKILL & AKTIONSLEISTE
# ═══════════════════════════════════════════════════════════════
var available_skills: Array = [] # Alle verfügbaren Skills der Klasse
var action_bar: Array = [] # 9 Slots: skill_id (String) oder Consumable oder null
var skill_cooldowns: Dictionary = {} # skill_id -> verbleibende Zeit
var consumable_cooldowns: Dictionary = {} # item_name -> verbleibende Zeit
# Global Cooldown
var global_cooldown: float = 0.0
const GLOBAL_COOLDOWN_TIME: float = 1.5
# Autoattack
var autoattack_active: bool = false
const HEAVY_STRIKE_COOLDOWN: float = 3.0
const HEAVY_STRIKE_RANGE: float = 2.0
# Cast System
var is_casting: bool = false
var cast_time_remaining: float = 0.0
var cast_total: float = 0.0
var pending_skill_id: String = ""
# ═══════════════════════════════════════════════════════════════
# KAMPF
# ═══════════════════════════════════════════════════════════════
var target = null # Aktuell markierter Gegner
# ═══════════════════════════════════════════════════════════════
# ANIMATION
# ═══════════════════════════════════════════════════════════════
var anim_player: AnimationPlayer = null
var skeleton: Skeleton3D = null
var weapon_attachment: BoneAttachment3D = null
var current_weapon_model: Node3D = null
var is_attacking: bool = false
var is_dead: bool = false
var is_walking: bool = false
var is_rolling: bool = false
var roll_direction: Vector3 = Vector3.ZERO
var roll_cooldown: float = 0.0
const ROLL_COOLDOWN: float = 1.5
# Eingefrорener Kamerawinkel für Bewegungsberechnung:
# wird nur aktualisiert wenn LMB NICHT gedrückt ist, damit
# LMB-Kamerarotation die Laufrichtung nicht verändert
var _movement_yaw: float = 0.0
var _lmb_press_pos: Vector2 = Vector2.ZERO
# ═══════════════════════════════════════════════════════════════
# NODE-REFERENZEN
# ═══════════════════════════════════════════════════════════════
@onready var camera_pivot = $CameraPivot
@onready var camera = $CameraPivot/Camera3D
@onready var hud = $HUD
@onready var model = $Model
# UI-Panels (zur Laufzeit erstellt)
var loot_window = null
var inventory_panel = null
var skill_panel = null
var character_panel = null
# ═══════════════════════════════════════════════════════════════
# READY
# ═══════════════════════════════════════════════════════════════
func _ready():
add_to_group("player")
# Jolt Physics: Boden sicher erkennen
floor_snap_length = 0.3
floor_max_angle = deg_to_rad(50.0)
# Aktionsleiste mit 9 leeren Slots initialisieren
action_bar.resize(9)
for i in range(9):
action_bar[i] = null
# Animationen einrichten
_setup_animations()
# UI-Panels erstellen
_create_ui_panels()
# HUD Signale verbinden
hud.slot_clicked.connect(_on_slot_clicked)
hud.slot_drag_removed.connect(_on_slot_drag_removed)
hud.slot_drag_swapped.connect(_on_slot_drag_swapped)
hud.update_health(current_hp, max_hp)
# ═══════════════════════════════════════════════════════════════
# UI-PANELS
# ═══════════════════════════════════════════════════════════════
func _create_ui_panels():
loot_window = LOOT_WINDOW_SCENE.instantiate()
add_child(loot_window)
loot_window.setup(self)
inventory_panel = INVENTORY_PANEL_SCENE.instantiate()
add_child(inventory_panel)
inventory_panel.setup(self)
skill_panel = SKILL_PANEL_SCENE.instantiate()
add_child(skill_panel)
skill_panel.setup(self)
character_panel = CHARACTER_PANEL_SCENE.instantiate()
add_child(character_panel)
# Gold-Änderungen an HUD weiterleiten
inventory.gold_changed.connect(func(amount): hud.update_gold(amount))
# ═══════════════════════════════════════════════════════════════
# KLASSEN-INITIALISIERUNG
# ═══════════════════════════════════════════════════════════════
func _init_class_skills():
available_skills.clear()
# Autoattack ist für alle Klassen verfügbar
available_skills.append({
"id": "autoattack",
"name": "Autoattack",
"description": "Grundangriff mit ausgerüsteter Waffe.",
"icon": "res://icons/autoattack_icon.svg",
"cooldown": 0.0,
"cast_time": 0.0,
})
if character_class == null:
return
match character_class.main_stat:
CharacterClass.MainStat.STRENGTH: # Krieger
available_skills.append({
"id": "heavy_strike",
"name": "Mächtiger Schlag",
"description": "Schlägt den Gegner mit voller Kraft (10-15 Schaden, 3s CD).",
"icon": "res://icons/heavy_strike_icon.svg",
"cooldown": HEAVY_STRIKE_COOLDOWN,
"cast_time": 0.0,
})
CharacterClass.MainStat.INTELLIGENCE: # Magier
available_skills.append({
"id": "frostbolt",
"name": "Frostblitz",
"description": "Schießt einen Frostblitz auf den Feind (2s Channeling).",
"icon": "res://icons/heavy_strike_icon.svg", # Temp-Icon
"cooldown": 0.0,
"cast_time": 2.0,
})
CharacterClass.MainStat.AGILITY: # Schurke
available_skills.append({
"id": "quick_strike",
"name": "Schnellschlag",
"description": "Blitzschneller Angriff (1s CD).",
"icon": "res://icons/autoattack_icon.svg", # Temp-Icon
"cooldown": 1.0,
"cast_time": 0.0,
})
# Standard-Belegung: Slot 0 = Autoattack, Slot 1 = erster Skill
action_bar[0] = "autoattack"
if available_skills.size() > 1:
action_bar[1] = available_skills[1]["id"]
# ═══════════════════════════════════════════════════════════════
# STAT-BERECHNUNG
# ═══════════════════════════════════════════════════════════════
func _calculate_stats():
if character_class == null:
return
# Basis-Stats aus Klasse + Level
strength = character_class.base_strength + int(character_class.strength_per_level * (level - 1))
agility = character_class.base_agility + int(character_class.agility_per_level * (level - 1))
intelligence = character_class.base_intelligence + int(character_class.intelligence_per_level * (level - 1))
stamina = character_class.base_stamina + int(character_class.stamina_per_level * (level - 1))
# Stats aus Equipment addieren
armor = 0
for slot in equipment:
var item = equipment[slot]
if item == null:
continue
strength += item.strength
agility += item.agility
intelligence += item.intelligence
stamina += item.stamina
armor += item.armor
# HP aus Stamina berechnen
var old_max = max_hp
max_hp = stamina * CharacterClass.HP_PER_STAMINA
# HP proportional anpassen
if old_max > 0:
current_hp = int(current_hp * float(max_hp) / float(old_max))
current_hp = clamp(current_hp, 0, max_hp)
# Ressource berechnen
match character_class.resource_type:
CharacterClass.ResourceType.MANA:
max_resource = character_class.base_resource + intelligence * CharacterClass.MANA_PER_INT
CharacterClass.ResourceType.RAGE, CharacterClass.ResourceType.ENERGY:
max_resource = character_class.base_resource
_:
max_resource = 0
current_resource = clamp(current_resource, 0, max_resource)
func get_resource_name() -> String:
if character_class == null:
return ""
match character_class.resource_type:
CharacterClass.ResourceType.MANA: return "Mana"
CharacterClass.ResourceType.RAGE: return "Wut"
CharacterClass.ResourceType.ENERGY: return "Energie"
return ""
# ═══════════════════════════════════════════════════════════════
# EQUIPMENT
# ═══════════════════════════════════════════════════════════════
# Item anlegen — gibt das alte Item zurück (oder null)
func equip_item(item: Equipment) -> Equipment:
var slot = item.slot
var old_item = equipment[slot]
equipment[slot] = item
_calculate_stats()
hud.update_health(current_hp, max_hp)
if max_resource > 0:
hud.update_resource(current_resource, max_resource, get_resource_name())
if character_panel and character_panel.panel_visible:
character_panel.update_stats(self)
# Waffen-Modell aktualisieren
if slot == Equipment.Slot.WEAPON and weapon_attachment != null:
_attach_weapon_model(item)
return old_item
func get_equipped_weapon() -> Equipment:
return equipment[Equipment.Slot.WEAPON]
func get_attack_damage() -> int:
var weapon = get_equipped_weapon()
if weapon == null:
if character_class:
return randi_range(character_class.unarmed_min_damage, character_class.unarmed_max_damage)
return 1
# Main-Stat-Bonus
var bonus = 0
if character_class:
match character_class.main_stat:
CharacterClass.MainStat.STRENGTH: bonus = int(strength * CharacterClass.DAMAGE_PER_MAIN_STAT)
CharacterClass.MainStat.AGILITY: bonus = int(agility * CharacterClass.DAMAGE_PER_MAIN_STAT)
CharacterClass.MainStat.INTELLIGENCE: bonus = int(intelligence * CharacterClass.DAMAGE_PER_MAIN_STAT)
return randi_range(weapon.min_damage, weapon.max_damage) + bonus
func get_attack_range() -> float:
var weapon = get_equipped_weapon()
if weapon == null:
return 1.5
return weapon.weapon_range
func get_attack_cooldown() -> float:
var weapon = get_equipped_weapon()
if weapon == null:
if character_class:
return character_class.unarmed_attack_speed
return 1.5
var speed = weapon.attack_speed
if character_class:
speed *= max(0.1, 1.0 - (agility * 0.002)) # Agilität reduziert Cooldown leicht
return speed
func get_dps() -> float:
var weapon = get_equipped_weapon()
var avg_dmg: float
if weapon:
avg_dmg = float(weapon.min_damage + weapon.max_damage) / 2.0
elif character_class:
avg_dmg = float(character_class.unarmed_min_damage + character_class.unarmed_max_damage) / 2.0
else:
avg_dmg = 1.5
var cooldown = get_attack_cooldown()
if cooldown <= 0:
return 0.0
return avg_dmg / cooldown
# ═══════════════════════════════════════════════════════════════
# HP / HEILUNG / TOD
# ═══════════════════════════════════════════════════════════════
func take_damage(amount: int):
var effective = max(1, amount - int(armor * 0.1))
current_hp = clamp(current_hp - effective, 0, max_hp)
hud.update_health(current_hp, max_hp)
if current_hp <= 0:
die()
func heal(amount: int):
current_hp = clamp(current_hp + amount, 0, max_hp)
hud.update_health(current_hp, max_hp)
func restore_mana(amount: int):
current_resource = clamp(current_resource + amount, 0, max_resource)
hud.update_resource(current_resource, max_resource, get_resource_name())
func die():
if is_dead:
return
is_dead = true
print("Spieler gestorben!")
_play_anim("death")
# ═══════════════════════════════════════════════════════════════
# XP / LEVELING
# ═══════════════════════════════════════════════════════════════
func gain_xp(amount: int):
current_xp += amount
print("+" + str(amount) + " XP")
while current_xp >= xp_to_next_level:
current_xp -= xp_to_next_level
_level_up()
hud.update_level(level, current_xp, xp_to_next_level)
func _level_up():
level += 1
xp_to_next_level = int(xp_to_next_level * 1.5)
_calculate_stats()
current_hp = max_hp
current_resource = max_resource
hud.update_health(current_hp, max_hp)
hud.update_resource(current_resource, max_resource, get_resource_name())
print("LEVEL UP! Jetzt Level " + str(level))
# ═══════════════════════════════════════════════════════════════
# LOOT
# ═══════════════════════════════════════════════════════════════
func receive_loot(loot: Dictionary, world_pos: Vector3):
if loot_window:
loot_window.show_loot(loot, world_pos)
# ═══════════════════════════════════════════════════════════════
# AKTIONSLEISTE
# ═══════════════════════════════════════════════════════════════
# Skill auf Slot legen
func assign_skill_to_action_bar(slot_index: int, skill_id: String):
if slot_index < 0 or slot_index >= 9:
return
action_bar[slot_index] = skill_id
_refresh_action_slot(slot_index)
# Consumable auf Slot legen
func assign_to_action_bar(slot_index: int, item: Consumable):
if slot_index < 0 or slot_index >= 9:
return
action_bar[slot_index] = item
_refresh_action_slot(slot_index)
# Slot-Icon und Cooldown im HUD aktualisieren
func _refresh_action_slot(slot_index: int):
if slot_index < 0 or slot_index >= 9:
return
var slot_content = action_bar[slot_index]
if slot_content == null:
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
return
if slot_content is String:
# Skill
var skill = _find_skill(slot_content)
if skill:
if skill["icon"] != "":
hud.set_slot_icon(slot_index, skill["icon"])
hud.set_slot_stack_count(slot_index, 0)
elif slot_content is Consumable:
# Consumable
if slot_content.icon:
hud.set_slot_icon_texture(slot_index, slot_content.icon)
hud.set_slot_stack_count(slot_index, slot_content.stack_size)
# Stack-Counts aller Consumable-Slots aktualisieren
func _update_action_bar_stacks():
for i in range(9):
if action_bar[i] is Consumable:
hud.set_slot_stack_count(i, action_bar[i].stack_size)
# Slot geleert per Drag
func _on_slot_drag_removed(slot_index: int):
action_bar[slot_index] = null
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
# Zwei Slots tauschen per Drag
func _on_slot_drag_swapped(from_slot: int, to_slot: int):
var temp = action_bar[from_slot]
action_bar[from_slot] = action_bar[to_slot]
action_bar[to_slot] = temp
_refresh_action_slot(from_slot)
_refresh_action_slot(to_slot)
func _find_skill(skill_id: String) -> Dictionary:
for s in available_skills:
if s["id"] == skill_id:
return s
return {}
# ═══════════════════════════════════════════════════════════════
# SKILL-AUSFÜHRUNG
# ═══════════════════════════════════════════════════════════════
func _on_slot_clicked(slot_index: int):
var slot_content = action_bar[slot_index]
if slot_content == null:
return
if slot_content is String:
execute_skill(slot_content)
elif slot_content is Consumable:
_use_consumable_slot(slot_index, slot_content)
func execute_skill(skill_id: String):
if is_dead or is_casting:
return
var skill = _find_skill(skill_id)
if skill.is_empty():
return
# GCD Check
if global_cooldown > 0:
print("GCD aktiv: %.1fs" % global_cooldown)
return
# Skill-CD Check
if skill_cooldowns.get(skill_id, 0.0) > 0:
print(skill["name"] + " im Cooldown: %.1fs" % skill_cooldowns[skill_id])
return
# Ziel-Check für Angriffe
var needs_target = (skill_id != "")
if skill_id in ["autoattack", "heavy_strike", "quick_strike", "frostbolt"]:
if target == null or not is_instance_valid(target):
print("Kein Ziel!")
return
# Cast-Zeit?
if skill["cast_time"] > 0:
_start_cast(skill_id, skill["cast_time"])
return
_apply_skill(skill_id)
func _start_cast(skill_id: String, cast_time: float):
is_casting = true
cast_total = cast_time
cast_time_remaining = cast_time
pending_skill_id = skill_id
hud.show_castbar(skill_id, cast_time)
func _cancel_cast():
is_casting = false
cast_time_remaining = 0.0
pending_skill_id = ""
hud.hide_castbar()
func _apply_skill(skill_id: String):
match skill_id:
"autoattack":
start_autoattack()
perform_autoattack()
"heavy_strike":
_do_heavy_strike()
"quick_strike":
_do_quick_strike()
"frostbolt":
_do_frostbolt()
# ─── Autoattack ───────────────────────────────────────────────
func start_autoattack():
autoattack_active = true
func stop_autoattack():
autoattack_active = false
func perform_autoattack():
if target == null or not is_instance_valid(target):
target = null
autoattack_active = false
return
var distance = global_position.distance_to(target.global_position)
if distance <= get_attack_range():
var dmg = get_attack_damage()
target.take_damage(dmg)
print("Autoattack: " + str(dmg) + " Schaden")
trigger_global_cooldown()
_play_anim_once("autoattack")
else:
print("Ziel zu weit entfernt")
# ─── Heavy Strike ─────────────────────────────────────────────
func _do_heavy_strike():
if target == null or not is_instance_valid(target):
return
var distance = global_position.distance_to(target.global_position)
if distance > HEAVY_STRIKE_RANGE:
print("Ziel zu weit für Heavy Strike!")
return
var damage = randi_range(10, 15) + int(strength * 0.5)
target.take_damage(damage)
skill_cooldowns["heavy_strike"] = HEAVY_STRIKE_COOLDOWN
trigger_global_cooldown()
start_autoattack()
print("Heavy Strike! " + str(damage) + " Schaden")
_play_anim_once("heavy_strike")
# ─── Quick Strike (Schurke) ───────────────────────────────────
func _do_quick_strike():
if target == null or not is_instance_valid(target):
return
var distance = global_position.distance_to(target.global_position)
if distance > get_attack_range():
print("Ziel zu weit für Quick Strike!")
return
var damage = get_attack_damage()
target.take_damage(damage)
skill_cooldowns["quick_strike"] = 1.0
trigger_global_cooldown()
start_autoattack()
print("Quick Strike! " + str(damage) + " Schaden")
_play_anim_once("autoattack")
# ─── Frostbolt (Magier) ───────────────────────────────────────
func _do_frostbolt():
if target == null or not is_instance_valid(target):
return
var damage = randi_range(8, 14) + int(intelligence * 0.7)
target.take_damage(damage)
trigger_global_cooldown()
start_autoattack()
print("Frostblitz! " + str(damage) + " Schaden")
hud.hide_castbar()
# ─── Consumable ───────────────────────────────────────────────
func use_consumable(item: Consumable) -> bool:
# Cooldown prüfen
if consumable_cooldowns.get(item.item_name, 0.0) > 0:
print(item.item_name + " im Cooldown!")
return false
var used = item.use(self)
if used:
consumable_cooldowns[item.item_name] = item.cooldown
return used
func _use_consumable_slot(slot_index: int, item: Consumable):
if use_consumable(item):
if item.stack_size <= 0:
# Stack leer: Item aus Inventar und Slot entfernen
inventory.remove_item(item)
action_bar[slot_index] = null
hud.clear_slot_icon(slot_index)
else:
hud.set_slot_stack_count(slot_index, item.stack_size)
# ─── GCD ──────────────────────────────────────────────────────
func trigger_global_cooldown():
global_cooldown = get_attack_cooldown()
# ═══════════════════════════════════════════════════════════════
# ZIELAUSWAHL
# ═══════════════════════════════════════════════════════════════
func set_target(new_target, start_attack: bool = false):
if target != null and is_instance_valid(target):
target.hide_health()
target = new_target
target.show_health()
print("Ziel: " + target.name)
if start_attack:
start_autoattack()
func clear_target():
if target != null and is_instance_valid(target):
target.hide_health()
target = null
autoattack_active = false
print("Ziel aufgehoben")
func _try_select_target(start_attack: bool = false):
var space_state = get_world_3d().direct_space_state
var viewport = get_viewport()
var mouse_pos = viewport.get_mouse_position()
var ray_origin = camera.project_ray_origin(mouse_pos)
var ray_end = ray_origin + camera.project_ray_normal(mouse_pos) * 100.0
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
query.exclude = [self]
var result = space_state.intersect_ray(query)
if result and result.collider.has_method("take_damage"):
set_target(result.collider, start_attack)
else:
# Klick auf freie Fläche → Target entfernen
clear_target()
# ═══════════════════════════════════════════════════════════════
# ANIMATION SETUP
# ═══════════════════════════════════════════════════════════════
func _setup_animations():
# AnimationPlayer aus importiertem Modell suchen
if model:
anim_player = model.find_child("AnimationPlayer", true, false) as AnimationPlayer
if anim_player == null:
push_warning("Player: Kein AnimationPlayer gefunden! Prüfe die Modell-Struktur.")
return
_load_anim_from_fbx(ANIM_IDLE, "idle", true)
_load_anim_from_fbx(ANIM_WALK, "walk", true)
_load_anim_from_fbx(ANIM_RUN, "run", true)
_load_anim_from_fbx(ANIM_JUMP, "jump", false)
_load_anim_from_fbx(ANIM_AUTOATTACK, "autoattack", false)
_load_anim_from_fbx(ANIM_HEAVY_STRIKE, "heavy_strike", false)
_load_anim_from_fbx(ANIM_DEATH, "death", false)
_load_anim_from_fbx(ANIM_WALK_BACK, "walk_back", true)
_load_anim_from_fbx(ANIM_STRAFE_LEFT, "strafe_left", true)
_load_anim_from_fbx(ANIM_STRAFE_RIGHT, "strafe_right", true)
_load_anim_from_fbx(ANIM_RUN_STRAFE_LEFT, "run_strafe_left", true)
_load_anim_from_fbx(ANIM_RUN_STRAFE_RIGHT, "run_strafe_right", true)
_load_anim_from_fbx(ANIM_WALK_JUMP, "walk_jump", false)
_load_anim_from_fbx(ANIM_RUN_JUMP, "run_jump", false)
_load_anim_from_fbx(ANIM_ROLL, "roll", false)
_load_anim_from_fbx(ANIM_TURN_180, "turn_180", false)
_play_anim("idle")
# Waffen-Attachment einrichten
_setup_weapon_attachment()
func _setup_weapon_attachment():
if model == null:
print("Player Weapon: Kein Model!")
return
skeleton = model.find_child("Skeleton3D", true, false) as Skeleton3D
if skeleton == null:
push_warning("Player: Kein Skeleton3D gefunden!")
return
# Alle Bones auflisten zum Debuggen
print("Player Bones: ", skeleton.get_bone_count())
for i in range(skeleton.get_bone_count()):
if "Hand" in skeleton.get_bone_name(i) or "hand" in skeleton.get_bone_name(i):
print(" Bone ", i, ": ", skeleton.get_bone_name(i))
var bone_idx = skeleton.find_bone("mixamorig_RightHand")
if bone_idx == -1:
push_warning("Player: Bone 'mixamorig_RightHand' nicht gefunden!")
return
print("Player Weapon: RightHand Bone gefunden (idx=", bone_idx, ")")
weapon_attachment = BoneAttachment3D.new()
weapon_attachment.bone_name = "mixamorig_RightHand"
skeleton.add_child(weapon_attachment)
func _attach_weapon_model(weapon: Equipment):
_remove_weapon_model()
if weapon == null or weapon.model_scene == null:
print("Player Weapon: Kein Model an Waffe! model_scene=", weapon.model_scene if weapon else "null")
return
if weapon_attachment == null:
print("Player Weapon: Kein weapon_attachment!")
return
current_weapon_model = weapon.model_scene.instantiate()
current_weapon_model.rotation_degrees = Vector3(90, 180, 0)
current_weapon_model.position = Vector3(0.5, 0.2, 0)
current_weapon_model.scale = Vector3(0.8, 0.8, 0.8)
weapon_attachment.add_child(current_weapon_model)
print("Player Weapon: Schwert angehängt! Scale=", current_weapon_model.scale)
func _remove_weapon_model():
if current_weapon_model != null and is_instance_valid(current_weapon_model):
current_weapon_model.queue_free()
current_weapon_model = null
func _load_anim_from_fbx(fbx_path: String, anim_name: String, loop: bool = false):
var scene = load(fbx_path)
if scene == null:
push_warning("Player: FBX nicht geladen: " + fbx_path)
return
var instance = scene.instantiate()
var ext_ap = instance.find_child("AnimationPlayer", true, false) as AnimationPlayer
if ext_ap == null:
instance.queue_free()
push_warning("Player: Kein AnimationPlayer in " + fbx_path)
return
var anim_list = ext_ap.get_animation_list()
if anim_list.is_empty():
instance.queue_free()
return
var anim = ext_ap.get_animation(anim_list[0])
# Root Motion entfernen: verhindert Snapping beim Loop
_strip_root_motion(anim)
# Loop-Modus setzen
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
if not anim_player.has_animation_library(""):
anim_player.add_animation_library("", AnimationLibrary.new())
var lib = anim_player.get_animation_library("")
if not lib.has_animation(anim_name):
lib.add_animation(anim_name, anim)
instance.queue_free()
func _strip_root_motion(anim: Animation):
# Mixamo speichert Root Motion entweder auf dem Armature-Node (kein Subname)
# ODER auf dem Hips-Knochen (hat Subname, aber bewegt sich in XZ vorwärts).
# Beide Fälle werden hier behandelt.
for i in range(anim.get_track_count() - 1, -1, -1):
if anim.track_get_type(i) != Animation.TYPE_POSITION_3D:
continue
var np: NodePath = anim.track_get_path(i)
if np.get_subname_count() == 0:
# Node-Position-Track (Armature-Root) → komplett entfernen
anim.remove_track(i)
else:
# Knochen-Position-Track — prüfen ob er sich in XZ bewegt
var key_count = anim.track_get_key_count(i)
if key_count < 2:
continue
var first: Vector3 = anim.track_get_key_value(i, 0)
var last: Vector3 = anim.track_get_key_value(i, key_count - 1)
var xz_delta = Vector2(last.x - first.x, last.z - first.z).length()
if xz_delta > 0.01:
# Hat XZ-Bewegung (z.B. Hips-Knochen bei Mixamo) → XZ nullen, Y behalten
for k in range(key_count):
var v: Vector3 = anim.track_get_key_value(i, k)
anim.track_set_key_value(i, k, Vector3(0.0, v.y, 0.0))
# ═══════════════════════════════════════════════════════════════
# ANIMATION ABSPIELEN
# ═══════════════════════════════════════════════════════════════
func _play_anim(name: String, speed: float = 1.0):
if anim_player == null or not anim_player.has_animation(name):
return
if anim_player.current_animation == name and anim_player.speed_scale == speed:
return
anim_player.speed_scale = speed
anim_player.play(name, -1, 1.0, false)
func _play_anim_once(name: String):
if anim_player == null or not anim_player.has_animation(name):
return
is_attacking = true
anim_player.speed_scale = 1.0
anim_player.play(name, -1, 1.0, false)
await anim_player.animation_finished
is_attacking = false
func _do_roll():
# Bewegungsrichtung beim Roll-Start erfassen
var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var has_target = target != null and is_instance_valid(target)
if input_dir.length() > 0.1:
var ref_angle = rotation.y if (has_target or is_walking) else _movement_yaw
roll_direction = Vector3(input_dir.x, 0, input_dir.y).rotated(Vector3.UP, ref_angle).normalized()
# Spieler in Roll-Richtung drehen, Kamera dabei kompensieren
var old_rot = rotation.y
rotation.y = atan2(-roll_direction.x, -roll_direction.z)
camera_pivot.rotation.y -= (rotation.y - old_rot)
else:
# Kein Input: nach vorne rollen
roll_direction = -global_transform.basis.z.normalized()
is_rolling = true
roll_cooldown = ROLL_COOLDOWN
anim_player.speed_scale = 1.0
anim_player.play("roll", -1, 1.0, false)
await anim_player.animation_finished
is_rolling = false
func _update_movement_animation(is_moving: bool, input_dir: Vector2):
if is_attacking or is_dead or is_rolling:
return
var has_target = target != null and is_instance_valid(target)
if not is_on_floor():
if not is_walking:
_play_anim("run_jump")
elif is_moving:
_play_anim("walk_jump")
else:
_play_anim("jump")
elif is_moving:
if not has_target and not is_walking:
var rmb = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
if input_dir.y > 0.1:
_play_anim("walk_back")
elif rmb and input_dir.x < -0.1:
_play_anim("run_strafe_left")
elif rmb and input_dir.x > 0.1:
_play_anim("run_strafe_right")
else:
_play_anim("run")
elif input_dir.y > 0.1:
_play_anim("walk_back")
elif input_dir.x < -0.1:
_play_anim("strafe_left" if is_walking else "run_strafe_left")
elif input_dir.x > 0.1:
_play_anim("strafe_right" if is_walking else "run_strafe_right")
elif is_walking:
_play_anim("walk")
else:
_play_anim("run")
else:
_play_anim("idle")
# ═══════════════════════════════════════════════════════════════
# PHYSICS PROCESS (Hauptschleife)
# ═══════════════════════════════════════════════════════════════
func _physics_process(delta):
if is_dead:
return
# ── Cooldowns herunterzählen ──────────────────────────────
var gcd_was_active = global_cooldown > 0
if global_cooldown > 0:
global_cooldown = max(0.0, global_cooldown - delta)
for key in skill_cooldowns.keys():
skill_cooldowns[key] = max(0.0, skill_cooldowns[key] - delta)
for key in consumable_cooldowns.keys():
consumable_cooldowns[key] = max(0.0, consumable_cooldowns[key] - delta)
# ── Autoattack nach GCD oder wenn in Range ───────────────
if autoattack_active and global_cooldown <= 0 and not is_casting:
perform_autoattack()
# ── Cast-System ───────────────────────────────────────────
if is_casting:
cast_time_remaining -= delta
hud.update_castbar(cast_total - cast_time_remaining, cast_total)
if cast_time_remaining <= 0:
is_casting = false
_apply_skill(pending_skill_id)
pending_skill_id = ""
# ── HUD Cooldowns aktualisieren ───────────────────────────
for i in range(9):
var slot_content = action_bar[i]
if slot_content is String:
var cd = max(global_cooldown, skill_cooldowns.get(slot_content, 0.0))
hud.set_slot_cooldown(i, cd)
elif slot_content is Consumable:
hud.set_slot_cooldown(i, consumable_cooldowns.get(slot_content.item_name, 0.0))
# ── Schwerkraft ───────────────────────────────────────────
if not is_on_floor():
velocity.y -= GRAVITY * delta
# ── Springen ─────────────────────────────────────────────
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
if is_casting:
_cancel_cast()
# ── Zielauswahl (nur Klick, nicht Drag) ───────────────────
if Input.is_action_just_pressed("select_target"):
_lmb_press_pos = get_viewport().get_mouse_position()
if Input.is_action_just_released("select_target"):
var release_pos = get_viewport().get_mouse_position()
if _lmb_press_pos.distance_to(release_pos) < 5.0:
_try_select_target(false)
if Input.is_action_just_pressed("ui_right_mouse"):
_try_select_target(true)
# ── Aktionsleiste (Tasten 1-9) ────────────────────────────
for i in range(9):
if Input.is_action_just_pressed("action_" + str(i + 1)):
hud.set_active_slot(i)
_on_slot_clicked(i)
# ── Panel-Shortcuts ───────────────────────────────────────
if Input.is_action_just_pressed("toggle_inventory") and inventory_panel:
inventory_panel.toggle()
if Input.is_action_just_pressed("toggle_character") and character_panel:
character_panel.toggle()
if character_panel.panel_visible:
character_panel.update_stats(self)
if Input.is_action_just_pressed("toggle_skills") and skill_panel:
skill_panel.toggle()
# ── TEST ──────────────────────────────────────────────────
if Input.is_action_just_pressed("test_damage"):
take_damage(10)
# ── Roll Cooldown ─────────────────────────────────────────
if roll_cooldown > 0:
roll_cooldown = max(0.0, roll_cooldown - delta)
# ── Walk Toggle ───────────────────────────────────────────
if Input.is_action_just_pressed("walk_toggle"):
is_walking = !is_walking
# ── Bewegung ─────────────────────────────────────────────
# get_vector: x = links(-)/rechts(+), y = vor(-)/zurück(+)
var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# Cast unterbrechen bei Bewegung
if is_casting and input_dir.length() > 0.1:
_cancel_cast()
var is_moving = input_dir.length() > 0.1
var has_target = target != null and is_instance_valid(target)
# ── Ausweichrolle ─────────────────────────────────────────
if Input.is_action_just_pressed("roll") and is_on_floor() and roll_cooldown <= 0 and not is_rolling:
_do_roll()
# ── Bewegungsrichtung ────────────────────────────────────────────
# Souls: world_yaw (stabil, dreht sich nicht mit Spieler mit)
# Lock-On/Walk: rotation.y (Strafe relativ zur Spielerausrichtung)
var lmb = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
# Bewegungs-Referenzwinkel einfrieren wenn LMB gedrückt
if not lmb:
_movement_yaw = camera_pivot.world_yaw
var dir3: Vector3
if has_target or is_walking:
dir3 = Vector3(input_dir.x, 0, input_dir.y).rotated(Vector3.UP, rotation.y)
else:
dir3 = Vector3(input_dir.x, 0, input_dir.y).rotated(Vector3.UP, _movement_yaw)
# ── Souls-Rotation: Zielwinkel aus _movement_yaw berechnen ─────
# Nur vorwärts/diagonal (nicht S), nicht bei RMB, nicht Lock-On/Walk
if is_moving and not is_walking and not has_target and not is_rolling \
and input_dir.y < 0.1 and not Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
var desired_angle = atan2(-dir3.x, -dir3.z)
var diff = abs(angle_difference(rotation.y, desired_angle))
if diff > deg_to_rad(150.0):
rotation.y = desired_angle
else:
rotation.y = lerp_angle(rotation.y, desired_angle, delta * 12.0)
# ── Velocity ─────────────────────────────────────────────
var current_speed = SPEED if is_walking else SPRINT_SPEED
if is_rolling:
velocity.x = roll_direction.x * SPRINT_SPEED
velocity.z = roll_direction.z * SPRINT_SPEED
else:
velocity.x = dir3.x * current_speed
velocity.z = dir3.z * current_speed
_update_movement_animation(is_moving, input_dir)
move_and_slide()