- AoE Drehangriff, Radius 3.5m, kostet 40 Wut - Schaden: 15-25 + 70% Stärke an alle Gegner im Radius - Kein Slow — pure DPS, Counterpart zu Tektonischem Schlag - Cooldown 6s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1325 lines
53 KiB
GDScript
1325 lines
53 KiB
GDScript
# 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"
|
||
const ANIM_DRINKING = "res://assets/Animations/Drinking.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
|
||
|
||
# Wut-Verfall
|
||
const RAGE_DECAY_DELAY: float = 5.0 # Sekunden nach letztem Kampfkontakt
|
||
const RAGE_DECAY_RATE: float = 10.0 # Wut pro Sekunde Verfall
|
||
var _rage_decay_timer: float = 0.0
|
||
|
||
# Durchbeißen / Schildwall / Trotz
|
||
var is_defending: bool = false
|
||
var _defend_timer: float = 0.0
|
||
var _defend_mode: String = "" # "schildwall" oder "trotz"
|
||
|
||
# Blutrausch — Blutungs-DOT
|
||
var _bleed_target = null
|
||
var _bleed_timer: float = 0.0 # verbleibende Dauer
|
||
var _bleed_tick_timer: float = 0.0 # bis zum nächsten Tick
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 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
|
||
const TEKTONISCHER_SCHLAG_COOLDOWN: float = 8.0
|
||
const TEKTONISCHER_SCHLAG_RADIUS: float = 4.0
|
||
const TEKTONISCHER_SCHLAG_RAGE: int = 30
|
||
const DURCHBEISSEN_COOLDOWN: float = 12.0
|
||
const DURCHBEISSEN_DURATION: float = 4.0
|
||
const SCHILDWALL_BLOCK: float = 0.85 # 85% Schadensreduktion
|
||
const TROTZ_MAX_REDUCTION: float = 0.60 # max 60% Reduktion bei 0 HP
|
||
const BLUTRAUSCH_COOLDOWN: float = 10.0
|
||
const BLUTRAUSCH_RANGE: float = 2.5
|
||
const BLUTRAUSCH_RAGE: int = 25
|
||
const BLUTRAUSCH_DOT_DURATION: float = 8.0 # Blutungsdauer in Sekunden
|
||
const BLUTRAUSCH_DOT_INTERVAL: float = 1.0 # Schaden alle 1s
|
||
const BLUTRAUSCH_DOT_DAMAGE: int = 4 # Schaden pro Tick
|
||
const WIRBELWIND_COOLDOWN: float = 6.0
|
||
const WIRBELWIND_RADIUS: float = 3.5
|
||
const WIRBELWIND_RAGE: int = 40
|
||
|
||
# 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 is_drinking: 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. Generiert Wut.",
|
||
"icon": "res://icons/heavy_strike_icon.svg",
|
||
"cooldown": HEAVY_STRIKE_COOLDOWN,
|
||
"cast_time": 0.0,
|
||
})
|
||
if level >= 5:
|
||
available_skills.append({
|
||
"id": "tektonischer_schlag",
|
||
"name": "Tektonischer Schlag",
|
||
"description": "Erschüttert den Boden um dich herum. Schlägt alle Gegner in der Nähe und verlangsamt sie.",
|
||
"icon": "res://icons/tektonischer_schlag_icon.svg",
|
||
"cooldown": TEKTONISCHER_SCHLAG_COOLDOWN,
|
||
"cast_time": 0.0,
|
||
})
|
||
if level >= 20:
|
||
available_skills.append({
|
||
"id": "wirbelwind",
|
||
"name": "Wirbelwind",
|
||
"description": "Drehangriff mit voller Wucht. Trifft alle Gegner in der Nähe für hohen Schaden.",
|
||
"icon": "res://icons/wirbelwind_icon.svg",
|
||
"cooldown": WIRBELWIND_COOLDOWN,
|
||
"cast_time": 0.0,
|
||
})
|
||
if level >= 15:
|
||
available_skills.append({
|
||
"id": "blutrausch",
|
||
"name": "Blutrausch",
|
||
"description": "Reißt eine tiefe Wunde. Sofortschaden + Blutung über 8 Sekunden.",
|
||
"icon": "res://icons/blutrausch_icon.svg",
|
||
"cooldown": BLUTRAUSCH_COOLDOWN,
|
||
"cast_time": 0.0,
|
||
})
|
||
if level >= 10:
|
||
var has_offhand = _has_shield()
|
||
available_skills.append({
|
||
"id": "durchbeissen",
|
||
"name": "Schildwall" if has_offhand else "Trotz",
|
||
"description": "Schildwall: Blockiert großen Teil des eingehenden Schadens." if has_offhand else "Trotz: Schadensreduktion skaliert mit fehlendem Leben.",
|
||
"icon": "res://icons/durchbeissen_icon.svg",
|
||
"cooldown": DURCHBEISSEN_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))
|
||
if is_defending:
|
||
if _defend_mode == "schildwall":
|
||
effective = int(effective * (1.0 - SCHILDWALL_BLOCK))
|
||
elif _defend_mode == "trotz":
|
||
var missing_hp = 1.0 - (float(current_hp) / float(max_hp))
|
||
var reduction = missing_hp * TROTZ_MAX_REDUCTION
|
||
effective = int(effective * (1.0 - reduction))
|
||
effective = max(1, effective)
|
||
current_hp = clamp(current_hp - effective, 0, max_hp)
|
||
hud.update_health(current_hp, max_hp)
|
||
_gain_rage(15)
|
||
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 _gain_rage(amount: int):
|
||
if character_class == null or character_class.resource_type != CharacterClass.ResourceType.RAGE:
|
||
return
|
||
current_resource = clamp(current_resource + amount, 0, max_resource)
|
||
_rage_decay_timer = RAGE_DECAY_DELAY
|
||
hud.update_resource(current_resource, max_resource, get_resource_name())
|
||
|
||
func _spend_rage(amount: int) -> bool:
|
||
if current_resource < amount:
|
||
return false
|
||
current_resource -= amount
|
||
hud.update_resource(current_resource, max_resource, get_resource_name())
|
||
return true
|
||
|
||
func _update_rage_decay(delta: float):
|
||
if character_class == null or character_class.resource_type != CharacterClass.ResourceType.RAGE:
|
||
return
|
||
if current_resource <= 0:
|
||
return
|
||
if _rage_decay_timer > 0.0:
|
||
_rage_decay_timer -= delta
|
||
return
|
||
current_resource = max(0, current_resource - int(RAGE_DECAY_RATE * delta))
|
||
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 or is_drinking:
|
||
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
|
||
# Wut-Check für Krieger-Skills
|
||
if skill_id in ["tektonischer_schlag"]:
|
||
if current_resource < TEKTONISCHER_SCHLAG_RAGE:
|
||
print("Zu wenig Wut!")
|
||
return
|
||
if skill_id == "blutrausch":
|
||
if current_resource < BLUTRAUSCH_RAGE:
|
||
print("Zu wenig Wut!")
|
||
return
|
||
if skill_id == "wirbelwind":
|
||
if current_resource < WIRBELWIND_RAGE:
|
||
print("Zu wenig Wut!")
|
||
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()
|
||
"tektonischer_schlag":
|
||
_do_tektonischer_schlag()
|
||
"durchbeissen":
|
||
_do_durchbeissen()
|
||
"blutrausch":
|
||
_do_blutrausch()
|
||
"wirbelwind":
|
||
_do_wirbelwind()
|
||
|
||
# ─── 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)
|
||
_gain_rage(10)
|
||
skill_cooldowns["heavy_strike"] = HEAVY_STRIKE_COOLDOWN
|
||
trigger_global_cooldown()
|
||
start_autoattack()
|
||
print("Heavy Strike! " + str(damage) + " Schaden")
|
||
_play_anim_once("heavy_strike")
|
||
|
||
# ─── Tektonischer Schlag ──────────────────────────────────────
|
||
|
||
func _do_tektonischer_schlag():
|
||
if not _spend_rage(TEKTONISCHER_SCHLAG_RAGE):
|
||
return
|
||
var space_state = get_world_3d().direct_space_state
|
||
var shape = SphereShape3D.new()
|
||
shape.radius = TEKTONISCHER_SCHLAG_RADIUS
|
||
var query = PhysicsShapeQueryParameters3D.new()
|
||
query.shape = shape
|
||
query.transform = global_transform
|
||
query.exclude = [self]
|
||
var hits = space_state.intersect_shape(query)
|
||
var hit_count = 0
|
||
for hit in hits:
|
||
var body = hit.collider
|
||
if body.has_method("take_damage"):
|
||
var damage = randi_range(5, 10) + int(strength * 0.3)
|
||
body.take_damage(damage)
|
||
if body.has_method("apply_slow"):
|
||
body.apply_slow(0.4, 3.0) # 60% langsamer für 3s
|
||
hit_count += 1
|
||
skill_cooldowns["tektonischer_schlag"] = TEKTONISCHER_SCHLAG_COOLDOWN
|
||
trigger_global_cooldown()
|
||
print("Tektonischer Schlag! %d Gegner getroffen" % hit_count)
|
||
|
||
# ─── Wirbelwind ───────────────────────────────────────────────
|
||
|
||
func _do_wirbelwind():
|
||
if not _spend_rage(WIRBELWIND_RAGE):
|
||
return
|
||
var space_state = get_world_3d().direct_space_state
|
||
var shape = SphereShape3D.new()
|
||
shape.radius = WIRBELWIND_RADIUS
|
||
var query = PhysicsShapeQueryParameters3D.new()
|
||
query.shape = shape
|
||
query.transform = global_transform
|
||
query.exclude = [self]
|
||
var hits = space_state.intersect_shape(query)
|
||
var hit_count = 0
|
||
for hit in hits:
|
||
var body = hit.collider
|
||
if body.has_method("take_damage"):
|
||
var damage = randi_range(15, 25) + int(strength * 0.7)
|
||
body.take_damage(damage)
|
||
hit_count += 1
|
||
skill_cooldowns["wirbelwind"] = WIRBELWIND_COOLDOWN
|
||
trigger_global_cooldown()
|
||
print("Wirbelwind! %d Gegner getroffen" % hit_count)
|
||
|
||
# ─── Durchbeißen / Schildwall / Trotz ────────────────────────
|
||
|
||
# ─── Blutrausch ───────────────────────────────────────────────
|
||
|
||
func _do_blutrausch():
|
||
if target == null or not is_instance_valid(target):
|
||
return
|
||
if global_position.distance_to(target.global_position) > BLUTRAUSCH_RANGE:
|
||
print("Ziel zu weit für Blutrausch!")
|
||
return
|
||
if not _spend_rage(BLUTRAUSCH_RAGE):
|
||
return
|
||
var damage = randi_range(8, 14) + int(strength * 0.5)
|
||
target.take_damage(damage)
|
||
_bleed_target = target
|
||
_bleed_timer = BLUTRAUSCH_DOT_DURATION
|
||
_bleed_tick_timer = BLUTRAUSCH_DOT_INTERVAL
|
||
skill_cooldowns["blutrausch"] = BLUTRAUSCH_COOLDOWN
|
||
trigger_global_cooldown()
|
||
print("Blutrausch! %d Sofortschaden + Blutung" % damage)
|
||
|
||
func _has_shield() -> bool:
|
||
var offhand = equipment[Equipment.Slot.OFFHAND]
|
||
return offhand != null and offhand.item_type == Equipment.ItemType.SHIELD
|
||
|
||
func _do_durchbeissen():
|
||
var has_offhand = _has_shield()
|
||
_defend_mode = "schildwall" if has_offhand else "trotz"
|
||
is_defending = true
|
||
_defend_timer = DURCHBEISSEN_DURATION
|
||
skill_cooldowns["durchbeissen"] = DURCHBEISSEN_COOLDOWN
|
||
trigger_global_cooldown()
|
||
print("Durchbeißen! Modus: " + _defend_mode)
|
||
|
||
# ─── 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
|
||
if is_drinking:
|
||
return false
|
||
var used = item.use(self)
|
||
if used:
|
||
consumable_cooldowns[item.item_name] = item.cooldown
|
||
_play_drinking_anim()
|
||
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, clear_on_miss: bool = true):
|
||
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)
|
||
elif clear_on_miss:
|
||
# Klick auf freie Fläche → Target entfernen (nur bei LMB)
|
||
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)
|
||
_load_anim_from_fbx(ANIM_DRINKING, "drinking", 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 _play_drinking_anim():
|
||
if anim_player == null or not anim_player.has_animation("drinking"):
|
||
return
|
||
is_drinking = true
|
||
anim_player.speed_scale = 1.0
|
||
anim_player.play("drinking", 0.3, 0.7, false)
|
||
await anim_player.animation_finished
|
||
is_drinking = 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 or is_drinking:
|
||
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
|
||
|
||
# ── Wut-Verfall ───────────────────────────────────────────
|
||
_update_rage_decay(delta)
|
||
|
||
# ── Blutungs-DOT ──────────────────────────────────────────
|
||
if _bleed_timer > 0.0:
|
||
_bleed_timer -= delta
|
||
_bleed_tick_timer -= delta
|
||
if _bleed_tick_timer <= 0.0:
|
||
_bleed_tick_timer = BLUTRAUSCH_DOT_INTERVAL
|
||
if _bleed_target != null and is_instance_valid(_bleed_target):
|
||
_bleed_target.take_damage(BLUTRAUSCH_DOT_DAMAGE)
|
||
else:
|
||
_bleed_timer = 0.0
|
||
if _bleed_timer <= 0.0:
|
||
_bleed_target = null
|
||
|
||
# ── Defend-Timer ──────────────────────────────────────────
|
||
if is_defending:
|
||
_defend_timer -= delta
|
||
if _defend_timer <= 0.0:
|
||
is_defending = false
|
||
_defend_mode = ""
|
||
|
||
# ── 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, false)
|
||
|
||
# ── 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/Trinken unterbrechen bei Bewegung
|
||
if is_casting and input_dir.length() > 0.1:
|
||
_cancel_cast()
|
||
if is_drinking and input_dir.length() > 0.1:
|
||
is_drinking = false
|
||
|
||
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()
|