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