# 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 target = null # Aktuell markierter Gegner # 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 # 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 @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 func _ready(): # Stats aus Klasse berechnen _calculate_stats() current_hp = max_hp hud.update_health(current_hp, max_hp) hud.update_level(level, current_xp, xp_to_next_level) hud.set_active_slot(0) # Icons für Skills setzen hud.set_slot_icon(0, "res://icons/autoattack_icon.svg") # Slot 1: Autoattack hud.set_slot_icon(1, "res://icons/heavy_strike_icon.svg") # Slot 2: Heavy Strike # HUD-Klicks verbinden hud.slot_clicked.connect(_on_slot_clicked) # Inventar Panel initialisieren inventory_panel.setup(self) # Loot Window initialisieren loot_window.setup(self) # Gold im HUD aktualisieren wenn sich Gold ändert inventory.gold_changed.connect(func(amount): hud.update_gold(amount)) # 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 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 # Equipment-Boni hinzufügen _apply_equipment_stats() print("Stats berechnet - STR: ", strength, " AGI: ", agility, " INT: ", intelligence, " STA: ", stamina, " ARM: ", armor, " HP: ", max_hp) # 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 neu berechnen mit Equipment-Stamina max_hp = stamina * CharacterClass.HP_PER_STAMINA # 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 vollständig auffüllen bei Level-Up current_hp = max_hp hud.update_health(current_hp, max_hp) # Character Panel aktualisieren falls offen character_panel.update_stats(self) print("LEVEL UP! Jetzt Level ", level, " - HP 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): match slot_index: 0: # Autoattack manuell starten if target != null and global_cooldown <= 0: start_autoattack() perform_autoattack() 1: # Heavy Strike use_heavy_strike() # 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 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) # 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!") # 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 (unbewaffnet = 3.0) 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_autoattack() if global_cooldown <= 0: perform_autoattack() # 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 print("Ziel aufgehoben, Autoattack gestoppt") # Autoattack aktivieren func start_autoattack(): autoattack_active = true print("Autoattack aktiviert") # Autoattack deaktivieren func stop_autoattack(): autoattack_active = false print("Autoattack deaktiviert") # 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() # Neues Schadenssystem mit Rüstung und Level-Differenz if target.has_method("take_damage_from"): target.take_damage_from(dmg, level, true) # true = Nahkampf else: target.take_damage(dmg) print("Autoattack: ", dmg, " Schaden (GCD: %.2fs, DPS: %.1f)" % [get_current_gcd(), get_dps()]) # GCD auslösen basierend auf Waffengeschwindigkeit + Haste 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 trigger_global_cooldown() # GCD zurücksetzen damit Autoattack nicht sofort feuert start_autoattack() # Autoattack nach Skill automatisch aktivieren print("Heavy Strike! ", damage, " Rohschaden") # 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): # Global Cooldown herunterzählen (gilt für alle Aktionen) if global_cooldown > 0: global_cooldown -= delta # Wenn GCD bereit und Autoattack aktiv, versuche anzugreifen if global_cooldown <= 0 and autoattack_active: perform_autoattack() # Skill-Cooldowns herunterzählen if heavy_strike_cooldown > 0: heavy_strike_cooldown -= delta # HUD Cooldowns aktualisieren hud.set_slot_cooldown(0, global_cooldown) # Slot 1: GCD (Autoattack) hud.set_slot_cooldown(1, heavy_strike_cooldown) # Slot 2: Heavy Strike 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 # 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 if Input.is_action_just_pressed("action_1"): hud.set_active_slot(0) if target != null and global_cooldown <= 0: start_autoattack() perform_autoattack() if Input.is_action_just_pressed("action_2"): hud.set_active_slot(1) use_heavy_strike() if Input.is_action_just_pressed("action_3"): hud.set_active_slot(2) if Input.is_action_just_pressed("action_4"): hud.set_active_slot(3) if Input.is_action_just_pressed("action_5"): hud.set_active_slot(4) if Input.is_action_just_pressed("action_6"): hud.set_active_slot(5) if Input.is_action_just_pressed("action_7"): hud.set_active_slot(6) if Input.is_action_just_pressed("action_8"): hud.set_active_slot(7) if Input.is_action_just_pressed("action_9"): hud.set_active_slot(8) # 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() # 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 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 # 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()