DungeonCrawler/player.gd
2026-03-15 23:23:49 +01:00

1048 lines
33 KiB
GDScript

# Player.gd
# Steuert den Spielercharakter: Bewegung, Kamera, HP, Angriff, Zielauswahl
extends CharacterBody3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
# Charakter-Klasse und Level-System
@export var character_class: CharacterClass
var level: int = 1
var current_xp: int = 0
var xp_to_next_level: int = 100 # XP benötigt für Level 2
# Aktuelle Stats (berechnet aus Klasse + Level)
var strength: int = 10
var agility: int = 10
var intelligence: int = 10
var stamina: int = 10
var armor: int = 0 # Rüstung aus Ausrüstung
# Level-Differenz Konstanten
const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz
const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation
var max_hp = 100
var current_hp = 100
var max_resource = 0 # Klassen-Ressource (Mana/Energie/Wut), 0 = keine
var current_resource = 0
var target = null # Aktuell markierter Gegner
# Aktionsleiste: Skills (String) oder Consumables in Slots (0-8)
var action_bar_items: Array = [null, null, null, null, null, null, null, null, null]
# Alle verfügbaren Skills (für Fähigkeiten-Panel) — wird klassenabhängig befüllt
var available_skills: Array = []
# Skill-Definitionen pro Klasse
const AUTOATTACK_SKILL = {"id": "autoattack", "name": "Autoattack", "icon": "res://icons/autoattack_icon.svg", "description": "Greift das Ziel im Nahkampf an.\nSchaden: Waffenschaden + Main-Stat"}
const MELEE_SKILLS = [
{"id": "heavy_strike", "name": "Heavy Strike", "icon": "res://icons/heavy_strike_icon.svg", "description": "Starker Hieb mit 3s Cooldown.\nSchaden: 10-15 + Main-Stat\nReichweite: 4.0"},
]
const MAGE_SKILLS = [
{"id": "wand", "name": "Zauberstab", "icon": "res://icons/wand_icon.svg", "description": "Magischer Fernkampfangriff.\nSchaden: Waffenschaden + INT\nReichweite: 20.0\nIgnoriert Rüstung\nDeaktiviert Autoattack"},
{"id": "frostbolt", "name": "Frostblitz", "icon": "res://icons/frostbolt_icon.svg", "description": "Magischer Fernkampfangriff mit Castzeit.\nSchaden: 12-20 + INT\nManakosten: 20\nReichweite: 20.0\nCastzeit: 1.5s\nCooldown: 2.5s"},
]
var potion_cooldown: float = 0.0
const POTION_COOLDOWN_TIME = 1.0
# Equipment System
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 System
var inventory: Inventory = Inventory.new()
# Global Cooldown System (GCD) - gilt für alle Aktionen inkl. Autoattack
var global_cooldown = 0.0
const BASE_GCD = 1.5 # Basis-GCD in Sekunden (wird durch Haste modifiziert)
var haste: float = 0.0 # Angriffsgeschwindigkeits-Bonus (0.1 = 10% schneller)
# Autoattack System
var autoattack_active = false # Ob Autoattack aktiv ist
# Zauberstab System (Magier-Fernkampf, exklusiv mit Autoattack)
var wand_active = false
const WAND_RANGE = 20.0
# Skills System - individuelle Cooldowns (zusätzlich zum GCD)
var heavy_strike_cooldown = 0.0
const HEAVY_STRIKE_DAMAGE_MIN = 10
const HEAVY_STRIKE_DAMAGE_MAX = 15
const HEAVY_STRIKE_COOLDOWN = 3.0
const HEAVY_STRIKE_RANGE = 4.0
var frostbolt_cooldown = 0.0
const FROSTBOLT_DAMAGE_MIN = 12
const FROSTBOLT_DAMAGE_MAX = 20
const FROSTBOLT_COOLDOWN = 2.5
const FROSTBOLT_RANGE = 20.0
const FROSTBOLT_MANA_COST = 20
const FROSTBOLT_CAST_TIME = 1.5
# Cast-System
var is_casting = false
var cast_time_remaining = 0.0
var cast_time_total = 0.0
var cast_spell_id = "" # Welcher Zauber gecastet wird
# Animation System
const ANIMATION_FILES = {
"start_walk": "res://assets/animations/Start Walking.fbx",
"walk": "res://assets/animations/Walking.fbx",
"stop_walk": "res://assets/animations/Stop Walking.fbx",
"walk_back": "res://assets/animations/Walking Backwards.fbx",
"strafe_left": "res://assets/animations/Left Strafe Walking.fbx",
"strafe_right": "res://assets/animations/Right Strafe Walking.fbx",
"jump": "res://assets/animations/Jumping.fbx",
"autoattack": "res://assets/animations/Autoattack.fbx",
"heavy_strike": "res://assets/animations/Heavy Strike.fbx",
"die": "res://assets/animations/Dying Backwards.fbx",
"idle": "res://assets/animations/Idle.fbx",
}
# Animations-State für Walk-Kette
var walk_state: String = "" # "", "start", "walking", "stop"
var anim_player: AnimationPlayer = null
var current_anim: String = ""
@onready var camera_pivot = $CameraPivot
@onready var camera = $CameraPivot/Camera3D
@onready var hud = $HUD
@onready var character_panel = $CharacterPanel
@onready var inventory_panel = $InventoryPanel
@onready var loot_window = $LootWindow
@onready var skill_panel = $SkillPanel
func _ready():
# Stats aus Klasse berechnen
_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())
hud.update_level(level, current_xp, xp_to_next_level)
hud.set_active_slot(0)
# Skills klassenabhängig aufbauen
_init_class_skills()
# Aktionsleiste initialisieren (Skills + Items)
for i in range(9):
_refresh_action_slot(i)
# HUD-Klicks und Drag 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)
# Inventar Panel initialisieren
inventory_panel.setup(self)
# Loot Window initialisieren
loot_window.setup(self)
# Skill Panel initialisieren
skill_panel.setup(self)
# Gold im HUD aktualisieren wenn sich Gold ändert
inventory.gold_changed.connect(func(amount): hud.update_gold(amount))
# Animationen laden
_setup_animations()
# Animationen aus FBX-Dateien laden und in AnimationPlayer einbinden
func _setup_animations():
var model = get_node_or_null("PlayerModel")
if model == null:
return
# AnimationPlayer im Modell finden
anim_player = _find_node_by_class(model, "AnimationPlayer")
if anim_player == null:
anim_player = AnimationPlayer.new()
anim_player.name = "AnimationPlayer"
model.add_child(anim_player)
# AnimationLibrary holen oder erstellen
var lib: AnimationLibrary
if anim_player.has_animation_library(""):
lib = anim_player.get_animation_library("")
else:
lib = AnimationLibrary.new()
anim_player.add_animation_library("", lib)
# Animationen aus separaten FBX-Dateien laden
for anim_id in ANIMATION_FILES:
var scene = load(ANIMATION_FILES[anim_id]) as PackedScene
if scene == null:
continue
var instance = scene.instantiate()
var source_ap = _find_node_by_class(instance, "AnimationPlayer")
if source_ap:
var names = source_ap.get_animation_list()
if names.size() > 0:
var anim = source_ap.get_animation(names[0])
# Endlos-Animationen loopen
if anim_id in ["walk", "walk_back", "strafe_left", "strafe_right", "idle"]:
anim.loop_mode = Animation.LOOP_LINEAR
if lib.has_animation(anim_id):
lib.remove_animation(anim_id)
lib.add_animation(anim_id, anim)
instance.queue_free()
# Signal für Walk-Kette: Start Walking → Walking
anim_player.animation_finished.connect(_on_animation_finished)
# Callback wenn eine Animation fertig ist
func _on_animation_finished(anim_name: StringName):
if anim_name == "start_walk" and walk_state == "start":
# Start Walking fertig → Walking (loop) starten
walk_state = "walking"
current_anim = "walk"
anim_player.play("walk")
elif anim_name == "stop_walk" and walk_state == "stop":
# Stop Walking fertig → Idle
walk_state = ""
current_anim = "idle"
anim_player.play("idle")
# Rekursiv nach einem Node einer bestimmten Klasse suchen
func _find_node_by_class(node: Node, class_name_str: String) -> Node:
for child in node.get_children():
if child.get_class() == class_name_str:
return child
var result = _find_node_by_class(child, class_name_str)
if result:
return result
return null
# Animation basierend auf Bewegungszustand abspielen
func _update_animation(input_dir: Vector2):
if anim_player == null:
return
# Angriffs-/Death-Animation nicht unterbrechen
if anim_player.is_playing() and current_anim in ["autoattack", "heavy_strike", "die"]:
return
var is_walking_forward = input_dir.y < -0.1 or (input_dir.length() > 0 and abs(input_dir.y) <= 0.1 and abs(input_dir.x) <= 0.1)
var is_moving = input_dir.length() > 0
# Vorwärts-Laufen: Start Walking → Walking → Stop Walking
if is_walking_forward:
if walk_state == "":
# Loslaufen: Start Walking abspielen
walk_state = "start"
current_anim = "start_walk"
anim_player.play("start_walk")
elif walk_state == "start" and not anim_player.is_playing():
# Start Walking fertig → Walking (loop)
walk_state = "walking"
current_anim = "walk"
anim_player.play("walk")
elif walk_state == "stop":
# War gerade am Anhalten, wieder loslaufen
walk_state = "start"
current_anim = "start_walk"
anim_player.play("start_walk")
# walking state: walk loopt automatisch
return
# Nicht mehr vorwärts → Stop Walking wenn nötig
if walk_state == "start" or walk_state == "walking":
if not is_moving:
walk_state = "stop"
current_anim = "stop_walk"
anim_player.play("stop_walk")
return
else:
# Wechsel zu anderer Bewegung (rückwärts, strafe)
walk_state = ""
# Stop Walking läuft noch
if walk_state == "stop":
if anim_player.is_playing():
return # Warten bis Stop Walking fertig
walk_state = ""
# Andere Animationen
var new_anim = ""
if not is_on_floor():
new_anim = "jump"
elif is_moving:
if input_dir.y > 0.1:
new_anim = "walk_back"
elif input_dir.x < -0.1:
new_anim = "strafe_left"
elif input_dir.x > 0.1:
new_anim = "strafe_right"
else:
new_anim = "walk"
else:
new_anim = "idle"
# Wenn Animation ausgelaufen ist, zurücksetzen
if not anim_player.is_playing():
current_anim = ""
if new_anim != current_anim:
current_anim = new_anim
if anim_player.has_animation(new_anim):
anim_player.play(new_anim)
else:
anim_player.stop()
# Einmalige Animation abspielen (Angriff, Tod, etc.)
func _play_attack_anim(anim_name: String):
if anim_player == null:
return
if anim_player.has_animation(anim_name):
current_anim = anim_name
walk_state = "" # Walk-Kette unterbrechen
anim_player.stop()
anim_player.play(anim_name)
# Stats basierend auf Klasse und Level berechnen
func _calculate_stats():
if character_class == null:
# Fallback ohne Klasse
strength = 10
agility = 10
intelligence = 10
stamina = 10
max_hp = 100
max_resource = 0
return
# Stats = Basis + (Level-1) * Zuwachs pro Level
var levels_gained = level - 1
strength = character_class.base_strength + int(levels_gained * character_class.strength_per_level)
agility = character_class.base_agility + int(levels_gained * character_class.agility_per_level)
intelligence = character_class.base_intelligence + int(levels_gained * character_class.intelligence_per_level)
stamina = character_class.base_stamina + int(levels_gained * character_class.stamina_per_level)
# HP aus Stamina berechnen
max_hp = stamina * CharacterClass.HP_PER_STAMINA
# Klassen-Ressource berechnen
_calculate_resource()
# Equipment-Boni hinzufügen
_apply_equipment_stats()
print("Stats berechnet - STR: ", strength, " AGI: ", agility, " INT: ", intelligence, " STA: ", stamina, " ARM: ", armor, " HP: ", max_hp, " RES: ", max_resource)
# Skills klassenabhängig aufbauen
func _init_class_skills():
available_skills = [AUTOATTACK_SKILL.duplicate()]
if character_class and character_class.resource_type == CharacterClass.ResourceType.MANA:
# Magier: Autoattack + Zauberstab + Frostblitz
available_skills.append_array(MAGE_SKILLS.duplicate(true))
action_bar_items[0] = "wand"
action_bar_items[1] = "frostbolt"
action_bar_items[2] = "autoattack"
else:
# Krieger/Schurke: Autoattack + Heavy Strike
available_skills.append_array(MELEE_SKILLS.duplicate(true))
action_bar_items[0] = "autoattack"
action_bar_items[1] = "heavy_strike"
# Klassen-Ressource berechnen (Mana aus INT, Energie fix, Wut fix)
func _calculate_resource():
if character_class == null or character_class.resource_type == CharacterClass.ResourceType.NONE:
max_resource = 0
return
match character_class.resource_type:
CharacterClass.ResourceType.MANA:
max_resource = character_class.base_resource + intelligence * CharacterClass.MANA_PER_INT
CharacterClass.ResourceType.ENERGY:
max_resource = character_class.base_resource # Fix, skaliert nicht
CharacterClass.ResourceType.RAGE:
max_resource = character_class.base_resource # Fix, skaliert nicht
# Name der Klassen-Ressource
func get_resource_name() -> String:
if character_class == null:
return ""
match character_class.resource_type:
CharacterClass.ResourceType.MANA: return "Mana"
CharacterClass.ResourceType.ENERGY: return "Energie"
CharacterClass.ResourceType.RAGE: return "Wut"
return ""
# Equipment-Stats auf Charakter anwenden
func _apply_equipment_stats():
armor = 0
haste = 0.0
var bonus_str = 0
var bonus_agi = 0
var bonus_int = 0
var bonus_sta = 0
for slot in equipment.keys():
var item = equipment[slot]
if item != null:
armor += item.armor
haste += item.haste
bonus_str += item.strength
bonus_agi += item.agility
bonus_int += item.intelligence
bonus_sta += item.stamina
strength += bonus_str
agility += bonus_agi
intelligence += bonus_int
stamina += bonus_sta
# HP und Ressource neu berechnen mit Equipment-Boni
max_hp = stamina * CharacterClass.HP_PER_STAMINA
_calculate_resource()
# Equipment anlegen
func equip_item(item: Equipment) -> Equipment:
var old_item = equipment[item.slot]
equipment[item.slot] = item
_calculate_stats()
# HP proportional anpassen
if max_hp > 0:
current_hp = mini(current_hp, max_hp)
hud.update_health(current_hp, max_hp)
character_panel.update_stats(self)
print("Ausgerüstet: ", item.item_name, " in Slot ", Equipment.get_slot_name(item.slot))
return old_item
# Equipment ablegen
func unequip_slot(slot: Equipment.Slot) -> Equipment:
var old_item = equipment[slot]
if old_item == null:
return null
equipment[slot] = null
_calculate_stats()
current_hp = mini(current_hp, max_hp)
hud.update_health(current_hp, max_hp)
character_panel.update_stats(self)
print("Abgelegt: ", old_item.item_name)
return old_item
# Ausgerüstete Waffe holen
func get_equipped_weapon() -> Equipment:
return equipment[Equipment.Slot.WEAPON]
# Main-Stat für Schadensberechnung holen
func get_main_stat() -> int:
if character_class == null:
return 10
match character_class.main_stat:
CharacterClass.MainStat.STRENGTH:
return strength
CharacterClass.MainStat.AGILITY:
return agility
CharacterClass.MainStat.INTELLIGENCE:
return intelligence
return 10
# XP erhalten und Level-Up prüfen
func gain_xp(amount: int):
current_xp += amount
print("+" , amount, " XP (", current_xp, "/", xp_to_next_level, ")")
while current_xp >= xp_to_next_level:
_level_up()
hud.update_level(level, current_xp, xp_to_next_level)
# Level-Up durchführen
func _level_up():
current_xp -= xp_to_next_level
level += 1
xp_to_next_level = _calculate_xp_for_level(level + 1)
# Stats neu berechnen
_calculate_stats()
# HP und Ressource vollständig auffüllen bei Level-Up
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())
# Character Panel aktualisieren falls offen
character_panel.update_stats(self)
print("LEVEL UP! Jetzt Level ", level, " - HP und Ressource voll aufgefüllt!")
# XP-Kurve: Jedes Level braucht mehr XP
func _calculate_xp_for_level(target_level: int) -> int:
return 100 * target_level # Level 2: 100, Level 3: 200, etc.
# Handler für HUD-Slot-Klicks
func _on_slot_clicked(slot_index: int):
_use_action_slot(slot_index)
# Slot aus Aktionsleiste entfernen (rausgezogen) - Item/Skill bleibt verfügbar
func _on_slot_drag_removed(slot_index: int):
action_bar_items[slot_index] = null
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
print("Slot " + str(slot_index + 1) + " geleert")
# Zwei Slots tauschen
func _on_slot_drag_swapped(from_slot: int, to_slot: int):
var temp = action_bar_items[from_slot]
action_bar_items[from_slot] = action_bar_items[to_slot]
action_bar_items[to_slot] = temp
_refresh_action_slot(from_slot)
_refresh_action_slot(to_slot)
# Skill per ID auf Slot legen
func assign_skill_to_action_bar(slot_index: int, skill_id: String):
action_bar_items[slot_index] = skill_id
_refresh_action_slot(slot_index)
print(skill_id + " auf Slot " + str(slot_index + 1) + " gelegt")
# Skill-Info anhand ID holen
func get_skill_info(skill_id: String) -> Dictionary:
for skill in available_skills:
if skill["id"] == skill_id:
return skill
return {}
# Cooldown für einen Slot ermitteln
func _get_slot_cooldown(slot_index: int) -> float:
var entry = action_bar_items[slot_index]
if entry is String:
match entry:
"autoattack":
return global_cooldown
"wand":
return global_cooldown
"heavy_strike":
return heavy_strike_cooldown
"frostbolt":
return frostbolt_cooldown
elif entry is Consumable:
return potion_cooldown
return 0.0
func _refresh_action_slot(slot_index: int):
var entry = action_bar_items[slot_index]
if entry is String:
# Skill
var info = get_skill_info(entry)
if info.size() > 0:
hud.set_slot_icon(slot_index, info["icon"])
else:
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
elif entry is Consumable and entry.icon:
hud.set_slot_icon_texture(slot_index, entry.icon)
hud.set_slot_stack_count(slot_index, entry.stack_size)
else:
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
func _use_action_slot(slot_index: int):
var entry = action_bar_items[slot_index]
if entry is String:
# Skill ausführen
match entry:
"autoattack":
if target != null and global_cooldown <= 0:
wand_active = false # Zauberstab deaktivieren
start_autoattack()
perform_autoattack()
"wand":
if target != null and global_cooldown <= 0:
autoattack_active = false # Autoattack deaktivieren
start_wand()
perform_wand_attack()
"heavy_strike":
use_heavy_strike()
"frostbolt":
use_frostbolt()
elif entry is Consumable:
if use_consumable(entry):
if entry.stack_size <= 0:
inventory.remove_item(entry)
_update_action_bar_stacks()
# Schaden am Spieler abziehen und HP-Leiste aktualisieren
func take_damage(amount):
current_hp = clamp(current_hp - amount, 0, max_hp)
hud.update_health(current_hp, max_hp)
if is_casting:
_cancel_cast()
if current_hp <= 0:
die()
# Schaden mit Rüstung und Level-Differenz berechnen
func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int:
var damage = float(raw_damage)
# Rüstung reduziert nur Nahkampfschaden
if is_melee and armor > 0:
var armor_reduction = float(armor) / (float(armor) + 50.0)
damage = damage * (1.0 - armor_reduction)
# Level-Differenz Modifikator (Gegner höheres Level = mehr Schaden)
var level_diff = attacker_level - level
var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD)
damage = damage * (1.0 + level_mod)
return maxi(1, int(damage))
# Schaden mit vollem Schadenssystem nehmen
func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true):
var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee)
print("Spieler nimmt Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)")
take_damage(final_damage)
# HP heilen und HP-Leiste aktualisieren
func heal(amount):
current_hp = clamp(current_hp + amount, 0, max_hp)
hud.update_health(current_hp, max_hp)
# Ressource wiederherstellen (Mana/Energie/Wut)
func restore_mana(amount):
current_resource = clamp(current_resource + amount, 0, max_resource)
hud.update_resource(current_resource, max_resource, get_resource_name())
# Ressource verbrauchen
func spend_resource(amount) -> bool:
if current_resource < amount:
print("Nicht genug " + get_resource_name() + "!")
return false
current_resource = clamp(current_resource - amount, 0, max_resource)
hud.update_resource(current_resource, max_resource, get_resource_name())
return true
# Consumable benutzen (Trank etc.)
func use_consumable(consumable: Consumable) -> bool:
if potion_cooldown > 0:
print("Trank noch im Cooldown!")
return false
if consumable.use(self):
potion_cooldown = consumable.cooldown
if consumable.stack_size <= 0:
return true # Verbraucht
return false
# Consumable auf Aktionsleiste legen
func assign_to_action_bar(slot_index: int, consumable: Consumable):
if slot_index < 2 or slot_index > 8:
return # Slot 0+1 sind reserviert für Skills
action_bar_items[slot_index] = consumable
if consumable and consumable.icon:
hud.set_slot_icon_texture(slot_index, consumable.icon)
hud.set_slot_stack_count(slot_index, consumable.stack_size)
else:
hud.clear_slot_icon(slot_index)
hud.set_slot_stack_count(slot_index, 0)
# Aktionsleiste Stack-Counts aktualisieren
func _update_action_bar_stacks():
for i in range(2, 9):
var item = action_bar_items[i]
if item is Consumable:
if item.stack_size <= 0:
action_bar_items[i] = null
hud.clear_slot_icon(i)
hud.set_slot_stack_count(i, 0)
else:
hud.set_slot_stack_count(i, item.stack_size)
# Loot empfangen und Fenster anzeigen
func receive_loot(loot: Dictionary, world_pos: Vector3):
loot_window.show_loot(loot, world_pos)
func die():
print("Spieler gestorben!")
_play_attack_anim("die")
# Schaden basierend auf ausgerüsteter Waffe + Main-Stat Skalierung
func get_attack_damage() -> int:
var weapon = get_equipped_weapon()
var base_damage: int
if weapon == null:
# Unbewaffneter Schaden klassenabhängig
if character_class:
base_damage = randi_range(character_class.unarmed_min_damage, character_class.unarmed_max_damage)
else:
base_damage = 1
else:
base_damage = randi_range(weapon.min_damage, weapon.max_damage)
# Schaden skaliert mit Main-Stat
var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
return base_damage + stat_bonus
# Aktuellen GCD berechnen (mit Haste-Modifikator)
func get_current_gcd() -> float:
var weapon = get_equipped_weapon()
var base_speed: float
if weapon == null:
# Unbewaffnete Angriffsgeschwindigkeit klassenabhängig
if character_class:
base_speed = character_class.unarmed_attack_speed
else:
base_speed = BASE_GCD
else:
base_speed = weapon.attack_speed
# Haste reduziert den GCD: GCD = Basis / (1 + Haste)
# Bei 0.5 Haste (50%): 1.5s / 1.5 = 1.0s
return base_speed / (1.0 + haste)
# DPS berechnen (für Anzeige)
func get_dps() -> float:
var weapon = get_equipped_weapon()
var avg_damage: float
if weapon == null:
# Unbewaffneter Durchschnittsschaden klassenabhängig
if character_class:
avg_damage = (character_class.unarmed_min_damage + character_class.unarmed_max_damage) / 2.0
else:
avg_damage = 1.0
else:
avg_damage = (weapon.min_damage + weapon.max_damage) / 2.0
var stat_bonus = get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT
var total_damage = avg_damage + stat_bonus
var gcd = get_current_gcd()
# DPS = Schaden / GCD
return total_damage / gcd
# Reichweite basierend auf ausgerüsteter Waffe (Nahkampf)
func get_attack_range() -> float:
var weapon = get_equipped_weapon()
if weapon == null:
return 3.0
return weapon.weapon_range
# Ziel markieren — start_attack=true startet sofort die Autoattack
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 markiert: ", target.name)
if start_attack:
_start_default_attack()
# Ziel komplett aufheben und Autoattack stoppen
func clear_target():
if target != null and is_instance_valid(target):
target.hide_health()
target = null
autoattack_active = false
wand_active = false
print("Ziel aufgehoben, Angriff gestoppt")
# Standard-Angriff starten (Rechtsklick): Magier=Zauberstab, Rest=Autoattack
func _start_default_attack():
if character_class and character_class.resource_type == CharacterClass.ResourceType.MANA:
start_wand()
if global_cooldown <= 0:
perform_wand_attack()
else:
start_autoattack()
if global_cooldown <= 0:
perform_autoattack()
# Autoattack aktivieren
func start_autoattack():
autoattack_active = true
print("Autoattack aktiviert")
# Autoattack deaktivieren
func stop_autoattack():
autoattack_active = false
print("Autoattack deaktiviert")
# Zauberstab aktivieren (deaktiviert Autoattack)
func start_wand():
wand_active = true
autoattack_active = false
print("Zauberstab aktiviert")
# Zauberstab deaktivieren
func stop_wand():
wand_active = false
print("Zauberstab deaktiviert")
# Zauberstab-Angriff ausführen (Fernkampf, magisch)
func perform_wand_attack():
if target == null or not is_instance_valid(target):
target = null
wand_active = false
return
var distance = global_position.distance_to(target.global_position)
if distance <= WAND_RANGE:
var dmg = get_attack_damage()
if target.has_method("take_damage_from"):
target.take_damage_from(dmg, level, false) # Magisch, ignoriert Rüstung
else:
target.take_damage(dmg)
print("Zauberstab: ", dmg, " magischer Schaden")
_play_attack_anim("autoattack")
trigger_global_cooldown()
# Führt einen Autoattack aus (wird vom GCD-System aufgerufen)
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()
if target.has_method("take_damage_from"):
target.take_damage_from(dmg, level, true) # Nahkampf
else:
target.take_damage(dmg)
print("Autoattack: ", dmg, " Schaden (GCD: %.2fs, DPS: %.1f)" % [get_current_gcd(), get_dps()])
_play_attack_anim("autoattack")
trigger_global_cooldown()
# Global Cooldown auslösen (basierend auf Waffe + Haste)
func trigger_global_cooldown():
global_cooldown = get_current_gcd()
# Heavy Strike: Starker Angriff mit Cooldown
func use_heavy_strike():
if target == null or not is_instance_valid(target):
print("Kein Ziel für Heavy Strike!")
return
# Nur Skill-eigener Cooldown Check (kein GCD-Check!)
if heavy_strike_cooldown > 0:
print("Heavy Strike noch im Cooldown: ", "%.1f" % heavy_strike_cooldown, "s")
return
var distance = global_position.distance_to(target.global_position)
if distance > HEAVY_STRIKE_RANGE:
print("Ziel zu weit entfernt für Heavy Strike!")
return
var base_damage = randi_range(HEAVY_STRIKE_DAMAGE_MIN, HEAVY_STRIKE_DAMAGE_MAX)
var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
var damage = base_damage + stat_bonus
# Neues Schadenssystem mit Rüstung und Level-Differenz
if target.has_method("take_damage_from"):
target.take_damage_from(damage, level, true) # true = Nahkampf
else:
target.take_damage(damage)
heavy_strike_cooldown = HEAVY_STRIKE_COOLDOWN
_play_attack_anim("heavy_strike")
trigger_global_cooldown() # GCD zurücksetzen damit Autoattack nicht sofort feuert
start_autoattack() # Autoattack nach Skill automatisch aktivieren
print("Heavy Strike! ", damage, " Rohschaden")
# Frostblitz: Cast starten
func use_frostbolt():
if is_casting:
return # Bereits am Casten
if target == null or not is_instance_valid(target):
print("Kein Ziel für Frostblitz!")
return
if frostbolt_cooldown > 0:
print("Frostblitz noch im Cooldown: ", "%.1f" % frostbolt_cooldown, "s")
return
if current_resource < FROSTBOLT_MANA_COST:
print("Nicht genug Mana für Frostblitz! (", current_resource, "/", FROSTBOLT_MANA_COST, ")")
return
var distance = global_position.distance_to(target.global_position)
if distance > FROSTBOLT_RANGE:
print("Ziel zu weit entfernt für Frostblitz!")
return
# Cast starten
_start_cast("frostbolt", FROSTBOLT_CAST_TIME)
print("Frostblitz wird gewirkt... (", FROSTBOLT_CAST_TIME, "s)")
# Frostblitz: Schaden anwenden nach erfolgreichem Cast
func _finish_frostbolt():
if target == null or not is_instance_valid(target):
print("Ziel verloren!")
return
var distance = global_position.distance_to(target.global_position)
if distance > FROSTBOLT_RANGE:
print("Ziel zu weit entfernt!")
return
# Mana abziehen
spend_resource(FROSTBOLT_MANA_COST)
var base_damage = randi_range(FROSTBOLT_DAMAGE_MIN, FROSTBOLT_DAMAGE_MAX)
var stat_bonus = int(intelligence * CharacterClass.DAMAGE_PER_MAIN_STAT)
var damage = base_damage + stat_bonus
if target.has_method("take_damage_from"):
target.take_damage_from(damage, level, false)
else:
target.take_damage(damage)
frostbolt_cooldown = FROSTBOLT_COOLDOWN
trigger_global_cooldown()
start_wand() # Zauberstab nach Cast weiter aktiv
print("Frostblitz! ", damage, " magischer Schaden (", FROSTBOLT_MANA_COST, " Mana)")
# Cast-System
func _start_cast(spell_id: String, cast_time: float):
is_casting = true
cast_spell_id = spell_id
cast_time_total = cast_time
cast_time_remaining = cast_time
autoattack_active = false # Autoattack pausieren während Cast
hud.show_castbar(spell_id, cast_time)
func _cancel_cast():
if not is_casting:
return
is_casting = false
cast_spell_id = ""
cast_time_remaining = 0.0
hud.hide_castbar()
print("Zauber unterbrochen!")
func _finish_cast():
var spell = cast_spell_id
is_casting = false
cast_spell_id = ""
cast_time_remaining = 0.0
hud.hide_castbar()
# Fertigen Zauber ausführen
match spell:
"frostbolt":
_finish_frostbolt()
# Raycast von der Kamera auf Mausposition — trifft Gegner mit take_damage()
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)
elif not start_attack:
# Nur bei Linksklick ins Leere: Ziel deselektieren und Autoattack stoppen
# Rechtsklick wird für Kameradrehung verwendet
clear_target()
func _physics_process(delta):
# Cast-System
if is_casting:
cast_time_remaining -= delta
hud.update_castbar(cast_time_total - cast_time_remaining, cast_time_total)
if cast_time_remaining <= 0:
_finish_cast()
# Global Cooldown herunterzählen (gilt für alle Aktionen)
if global_cooldown > 0:
global_cooldown -= delta
# Wenn GCD bereit und nicht am Casten
if global_cooldown <= 0 and not is_casting:
if wand_active:
perform_wand_attack()
elif autoattack_active:
perform_autoattack()
# Skill-Cooldowns herunterzählen
if heavy_strike_cooldown > 0:
heavy_strike_cooldown -= delta
if frostbolt_cooldown > 0:
frostbolt_cooldown -= delta
if potion_cooldown > 0:
potion_cooldown -= delta
# HUD Cooldowns aktualisieren - generisch pro Slot
for i in range(9):
var cd = _get_slot_cooldown(i)
hud.set_slot_cooldown(i, cd)
# 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()
# Linksklick: nur markieren
if Input.is_action_just_pressed("select_target"):
_try_select_target(false)
# Rechtsklick: markieren + angreifen
if Input.is_action_just_pressed("ui_right_mouse"):
_try_select_target(true)
# Aktionsleiste 1-9 — alle generisch über _use_action_slot
for i in range(9):
var action_name = "action_" + str(i + 1)
if Input.is_action_just_pressed(action_name):
hud.set_active_slot(i)
_use_action_slot(i)
# TEST: T drücken = 10 Schaden
if Input.is_action_just_pressed("test_damage"):
take_damage(10)
# C drücken = Charakter-Panel öffnen/schließen
if Input.is_action_just_pressed("toggle_character"):
character_panel.update_stats(self)
character_panel.toggle()
# I drücken = Inventar öffnen/schließen
if Input.is_action_just_pressed("toggle_inventory"):
inventory_panel.toggle()
# P drücken = Fähigkeiten-Panel öffnen/schließen
if Input.is_action_just_pressed("toggle_skills"):
skill_panel.toggle()
# Eingabe
var input_dir = Vector2.ZERO
if Input.is_action_pressed("move_forward"):
input_dir.y -= 1
if Input.is_action_pressed("move_back"):
input_dir.y += 1
if Input.is_action_pressed("move_left"):
input_dir.x -= 1
if Input.is_action_pressed("move_right"):
input_dir.x += 1
# Bewegung unterbricht Cast
if is_casting and input_dir.length() > 0:
_cancel_cast()
# Bewegung relativ zur Kamera
var world_yaw = rotation.y + camera_pivot.rotation.y
var forward = Vector3(-sin(world_yaw), 0, -cos(world_yaw)).normalized()
var right = Vector3(cos(world_yaw), 0, -sin(world_yaw)).normalized()
var direction = (forward * -input_dir.y + right * input_dir.x)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
# Animation aktualisieren
_update_animation(input_dir)
# RMB gehalten: Spieler schaut in Kamerarichtung
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
rotation.y = world_yaw
camera_pivot.rotation.y = 0
move_and_slide()