diff --git a/PROJEKTDOKU.md b/PROJEKTDOKU.md index 5b714c5..372bef2 100644 --- a/PROJEKTDOKU.md +++ b/PROJEKTDOKU.md @@ -23,7 +23,9 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge | RMB gehalten | Kamera drehen, Spieler schaut mit | | Linksklick auf Gegner | Ziel markieren | | Rechtsklick auf Gegner | Ziel markieren + Autoattack starten | -| 1 – 9 | Aktionsleiste Slot auswählen | +| 1 | Autoattack manuell starten | +| 2 | Heavy Strike (starke Attacke) | +| 3 – 9 | Aktionsleiste Slots (noch frei) | | Leertaste | Springen | | T | (Test) 10 Schaden am Spieler | @@ -198,6 +200,31 @@ Definiert einen Angriff — Schaden kommt von der ausgerüsteten Waffe. - Ohne Waffe: 1 Schaden, 1.5s Cooldown, 1.5 Reichweite - Mit Waffe: Schaden = zufällig zwischen min/max, Speed und Range von der Waffe +### Skills + +#### Autoattack (Taste 1) +- **Schaden**: 1 (unbewaffnet) oder Waffenschaden +- **Cooldown**: 1.5s (unbewaffnet) oder Waffengeschwindigkeit +- **Reichweite**: 1.5 (unbewaffnet) oder Waffenreichweite +- **Icon**: Faust mit grünem Kreispfeil +- Manuell per Taste 1 starten oder automatisch per Rechtsklick + +#### Heavy Strike (Taste 2) +- **Schaden**: 10-15 (zufällig) +- **Cooldown**: 3 Sekunden +- **Reichweite**: 2.0 +- **Icon**: Schwert mit roten Schlaglinien +- Manuell aktivierbare starke Attacke für höheren Schaden + +### UI & Icons +- Aktionsleiste zeigt Icons für Skills an +- Icons werden dynamisch beim Spielstart geladen +- Slot 1: Autoattack Icon (Faust) +- Slot 2: Heavy Strike Icon (Schwert) +- Slots 3-9: Verfügbar für weitere Skills +- **Mausklick**: Alle Slots sind per Maus klickbar +- **Cooldown-Anzeige**: Dunkle Überlagerung mit verbleibender Zeit in Sekunden + ### Schadenstypen (geplant) - **PHYSICAL** – normaler Schaden - **FIRE** – Feuerschaden (z.B. Magier) diff --git a/enemy.gd b/enemy.gd index 7a60e6b..3091c2d 100644 --- a/enemy.gd +++ b/enemy.gd @@ -2,26 +2,95 @@ # Steuert den Gegner: KI-Bewegung zum Spieler, Angriff, HP, Zielanzeige extends CharacterBody3D +signal enemy_died(spawn_position: Vector3, xp_reward: int) + const SPEED = 3.0 +const PATROL_SPEED = 1.5 const GRAVITY = 9.8 -const ATTACK_DAMAGE = 5 const ATTACK_RANGE = 1.5 const ATTACK_COOLDOWN = 2.0 +const AGGRO_RANGE = 8.0 # Entfernung ab der der Gegner angreift +const PATROL_RADIUS = 5.0 # Radius um Spawn-Position für Patrol +const PATROL_WAIT_TIME = 2.0 # Wartezeit am Patrol-Punkt -var max_hp = 50 -var current_hp = 50 -var target = null # Ziel des Gegners (normalerweise der Spieler) +# 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 + +enum State { PATROL, CHASE, ATTACK } + +# Stats-System +@export var level: int = 1 +@export var base_strength: int = 8 +@export var base_stamina: int = 10 +@export var base_armor: int = 5 # Rüstung reduziert Nahkampfschaden + +# Berechnete Stats +var strength: int = 8 +var stamina: int = 10 +var armor: int = 5 +var max_hp: int = 100 +var current_hp: int = 100 +var attack_damage: int = 5 + +# XP-Belohnung (skaliert mit Level) +var xp_reward: int = 25 + +var target = null # Spieler-Referenz (wird von World gesetzt) var can_attack = true +var spawn_position: Vector3 # Ursprüngliche Spawn-Position +var current_state = State.PATROL +var patrol_target: Vector3 # Aktuelles Patrol-Ziel +var is_waiting = false # Ob Gegner am Patrol-Punkt wartet @onready var health_label = $HealthLabel func _ready(): + _calculate_stats() + current_hp = max_hp health_label.visible = false _update_label() + spawn_position = global_position + _pick_new_patrol_target() + +# Stats basierend auf Level berechnen +func _calculate_stats(): + var levels_gained = level - 1 + strength = base_strength + levels_gained * 2 + stamina = base_stamina + levels_gained * 3 + armor = base_armor + levels_gained * 2 + + # HP = Stamina * 10 + max_hp = stamina * 10 + # Schaden = Stärke / 2 + attack_damage = int(strength * 0.5) + 2 + # XP = 25 * Level + xp_reward = 25 * level + + print("Enemy Stats (Lv", level, ") - STR:", strength, " STA:", stamina, " ARM:", armor, " HP:", max_hp, " DMG:", attack_damage) + +# 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: + # Rüstungsreduktion: armor / (armor + 50) = Prozent Reduktion + # Bei 5 Rüstung: 5/55 = ~9% Reduktion + # Bei 20 Rüstung: 20/70 = ~29% Reduktion + var armor_reduction = float(armor) / (float(armor) + 50.0) + damage = damage * (1.0 - armor_reduction) + + # Level-Differenz Modifikator + 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)) # Mindestens 1 Schaden # HP-Label Text aktualisieren func _update_label(): - health_label.text = str(current_hp) + " / " + str(max_hp) + health_label.text = "Lv" + str(level) + " " + str(current_hp) + "/" + str(max_hp) # HP-Label anzeigen (wenn Gegner markiert wird) func show_health(): @@ -31,16 +100,26 @@ func show_health(): func hide_health(): health_label.visible = false -# Schaden nehmen und Label aktualisieren +# Schaden nehmen und Label aktualisieren (alte Methode für Kompatibilität) func take_damage(amount): current_hp -= amount _update_label() if current_hp <= 0: die() +# Schaden mit vollem Schadenssystem (Rüstung, Level-Differenz) +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("Eingehender Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)") + take_damage(final_damage) + # Gegner aus der Szene entfernen func die(): - print("Gegner besiegt!") + print("Gegner besiegt! +", xp_reward, " XP") + # XP an Spieler geben + if target and target.has_method("gain_xp"): + target.gain_xp(xp_reward) + enemy_died.emit(spawn_position, xp_reward) queue_free() func _physics_process(delta): @@ -51,29 +130,84 @@ func _physics_process(delta): move_and_slide() return - var distance = global_position.distance_to(target.global_position) + # Prüfe Distanz zum Spieler für Aggro + var distance_to_player = global_position.distance_to(target.global_position) - if distance <= ATTACK_RANGE: - # In Reichweite: angreifen - velocity.x = 0 - velocity.z = 0 - if can_attack: - _attack() - else: - # Direkt auf Ziel zubewegen - var direction = (target.global_position - global_position) - direction.y = 0 - direction = direction.normalized() - velocity.x = direction.x * SPEED - velocity.z = direction.z * SPEED - look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z)) + # State-Wechsel basierend auf Distanz + match current_state: + State.PATROL: + if distance_to_player <= AGGRO_RANGE: + current_state = State.CHASE + print("Gegner hat Spieler entdeckt!") + else: + _do_patrol() + State.CHASE: + if distance_to_player <= ATTACK_RANGE: + current_state = State.ATTACK + else: + _chase_player() + State.ATTACK: + if distance_to_player > ATTACK_RANGE: + current_state = State.CHASE + else: + velocity.x = 0 + velocity.z = 0 + if can_attack: + _attack() move_and_slide() +# Neues Patrol-Ziel in der Nähe der Spawn-Position wählen +func _pick_new_patrol_target(): + var angle = randf() * TAU # Zufälliger Winkel + var distance = randf_range(2.0, PATROL_RADIUS) + patrol_target = spawn_position + Vector3(cos(angle) * distance, 0, sin(angle) * distance) + +# Patrol-Verhalten: Zufällig herumlaufen +func _do_patrol(): + if is_waiting: + return + + var distance_to_patrol = global_position.distance_to(patrol_target) + + if distance_to_patrol <= 0.5: + # Am Ziel angekommen, warten und neues Ziel wählen + velocity.x = 0 + velocity.z = 0 + _wait_at_patrol_point() + else: + # Zum Patrol-Ziel laufen + var direction = (patrol_target - global_position) + direction.y = 0 + direction = direction.normalized() + velocity.x = direction.x * PATROL_SPEED + velocity.z = direction.z * PATROL_SPEED + look_at(Vector3(patrol_target.x, global_position.y, patrol_target.z)) + +# Am Patrol-Punkt warten +func _wait_at_patrol_point(): + is_waiting = true + await get_tree().create_timer(PATROL_WAIT_TIME).timeout + is_waiting = false + _pick_new_patrol_target() + +# Spieler verfolgen +func _chase_player(): + var direction = (target.global_position - global_position) + direction.y = 0 + direction = direction.normalized() + velocity.x = direction.x * SPEED + velocity.z = direction.z * SPEED + look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z)) + # Angriff mit Cooldown func _attack(): can_attack = false - target.take_damage(ATTACK_DAMAGE) - print("Gegner greift an!") + # Gegner verwendet auch das Schadenssystem mit Level-Differenz + if target.has_method("take_damage_from"): + target.take_damage_from(attack_damage, level, true) + else: + target.take_damage(attack_damage) + print("Gegner (Lv", level, ") greift an: ", attack_damage, " Schaden") await get_tree().create_timer(ATTACK_COOLDOWN).timeout can_attack = true diff --git a/enemy.tscn b/enemy.tscn index 823a15e..85f59db 100644 --- a/enemy.tscn +++ b/enemy.tscn @@ -1,6 +1,7 @@ [gd_scene format=3 uid="uid://cvojaeanxugfj"] [ext_resource type="PackedScene" uid="uid://tosl2au4emxt" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-d.glb" id="1_7k104"] +[ext_resource type="Script" uid="uid://bg5qs3pcfp7p7" path="res://enemy.gd" id="2_enemy"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4gyqm"] radius = 0.6 @@ -11,6 +12,7 @@ radius = 0.6 height = 3.0 [node name="Enemy" type="CharacterBody3D" unique_id=332011146] +script = ExtResource("2_enemy") [node name="character-d2" parent="." unique_id=846574684 instance=ExtResource("1_7k104")] transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0) diff --git a/hud.gd b/hud.gd index 32798cb..1676c3e 100644 --- a/hud.gd +++ b/hud.gd @@ -1,9 +1,15 @@ # HUD.gd -# Verwaltet die Spieler-UI: HP-Leiste, Aktionsleiste (Slots 1-9) +# Verwaltet die Spieler-UI: HP-Leiste, XP-Leiste, Aktionsleiste (Slots 1-9) extends CanvasLayer +signal slot_clicked(slot_index: int) + @onready var health_bar = $Control/HealthBar @onready var health_label = $Control/HealthBar/HealthLabel + +# Level/XP UI (wird dynamisch erstellt) +var level_label: Label +var xp_bar: ProgressBar @onready var action_slots = [ $Control/ActionBar/A1, $Control/ActionBar/A2, @@ -17,6 +23,112 @@ extends CanvasLayer ] var active_slot = 0 +var slot_icons = [] # TextureRect nodes für Icons +var slot_cooldown_overlays = [] # ColorRect für Cooldown-Anzeige +var slot_cooldown_labels = [] # Label für Cooldown-Text + +func _ready(): + _create_level_ui() + + for i in range(9): + # Icon erstellen + var icon = TextureRect.new() + icon.name = "Icon" + icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.custom_minimum_size = Vector2(40, 40) + icon.position = Vector2(5, 5) + action_slots[i].add_child(icon) + slot_icons.append(icon) + + # Cooldown-Overlay erstellen (dunkle Überlagerung) + var cooldown_overlay = ColorRect.new() + cooldown_overlay.name = "CooldownOverlay" + cooldown_overlay.color = Color(0, 0, 0, 0.7) + cooldown_overlay.size = Vector2(50, 50) + cooldown_overlay.position = Vector2(0, 0) + cooldown_overlay.visible = false + cooldown_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE + action_slots[i].add_child(cooldown_overlay) + slot_cooldown_overlays.append(cooldown_overlay) + + # Cooldown-Text erstellen + var cooldown_label = Label.new() + cooldown_label.name = "CooldownLabel" + cooldown_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + cooldown_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + cooldown_label.size = Vector2(50, 50) + cooldown_label.position = Vector2(0, 0) + cooldown_label.add_theme_font_size_override("font_size", 16) + cooldown_label.visible = false + cooldown_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + action_slots[i].add_child(cooldown_label) + slot_cooldown_labels.append(cooldown_label) + + # Button für Klicks erstellen + var button = Button.new() + button.name = "SlotButton" + button.flat = true + button.size = Vector2(50, 50) + button.position = Vector2(0, 0) + button.modulate = Color(1, 1, 1, 0) # Unsichtbar + var slot_index = i + button.pressed.connect(func(): _on_slot_clicked(slot_index)) + action_slots[i].add_child(button) + +# Slot-Klick Handler +func _on_slot_clicked(slot_index: int): + set_active_slot(slot_index) + slot_clicked.emit(slot_index) + +# Icon für einen Slot setzen +func set_slot_icon(slot_index: int, icon_path: String): + if slot_index >= 0 and slot_index < 9: + var texture = load(icon_path) + if texture: + slot_icons[slot_index].texture = texture + else: + print("Icon nicht gefunden: ", icon_path) + +# Cooldown für einen Slot anzeigen (remaining_time in Sekunden) +func set_slot_cooldown(slot_index: int, remaining_time: float): + if slot_index < 0 or slot_index >= 9: + return + + if remaining_time > 0: + slot_cooldown_overlays[slot_index].visible = true + slot_cooldown_labels[slot_index].visible = true + slot_cooldown_labels[slot_index].text = "%.1f" % remaining_time + else: + slot_cooldown_overlays[slot_index].visible = false + slot_cooldown_labels[slot_index].visible = false + +# Level/XP UI erstellen +func _create_level_ui(): + var control = $Control + + # Level Label + level_label = Label.new() + level_label.name = "LevelLabel" + level_label.position = Vector2(20, 55) + level_label.add_theme_font_size_override("font_size", 14) + level_label.text = "Level 1" + control.add_child(level_label) + + # XP Bar + xp_bar = ProgressBar.new() + xp_bar.name = "XPBar" + xp_bar.position = Vector2(80, 55) + xp_bar.size = Vector2(140, 18) + xp_bar.show_percentage = false + xp_bar.value = 0 + + # XP Bar Farbe (blau) + var xp_style = StyleBoxFlat.new() + xp_style.bg_color = Color(0.2, 0.4, 0.9, 1.0) + xp_bar.add_theme_stylebox_override("fill", xp_style) + + control.add_child(xp_bar) # HP-Leiste und Text aktualisieren func update_health(current_hp, max_hp): @@ -24,6 +136,14 @@ func update_health(current_hp, max_hp): health_bar.value = current_hp health_label.text = str(current_hp) + " / " + str(max_hp) +# Level und XP aktualisieren +func update_level(level: int, current_xp: int, xp_to_next: int): + if level_label: + level_label.text = "Lv " + str(level) + if xp_bar: + xp_bar.max_value = xp_to_next + xp_bar.value = current_xp + # Aktions-Slot kurz golden hervorheben (0.1s) func set_active_slot(index): action_slots[active_slot].self_modulate = Color(1, 1, 1) diff --git a/player.gd b/player.gd index 4ae3bed..9af95da 100644 --- a/player.gd +++ b/player.gd @@ -6,19 +6,216 @@ 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 can_attack = true var target = null # Aktuell markierter Gegner -var equipped_weapon = null # Ausgerüstete Waffe (null = unbewaffnet, Schaden = 1) + +# 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 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) + +# 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): @@ -27,6 +224,28 @@ func take_damage(amount): 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) @@ -35,23 +254,66 @@ func heal(amount): func die(): print("Spieler gestorben!") -# Schaden basierend auf ausgerüsteter Waffe (unbewaffnet = 1) +# Schaden basierend auf ausgerüsteter Waffe + Main-Stat Skalierung func get_attack_damage() -> int: - if equipped_weapon == null: - return 1 - return randi_range(equipped_weapon.min_damage, equipped_weapon.max_damage) + 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) -# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5) + # 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: - if equipped_weapon == null: - return 1.5 - return equipped_weapon.range - -# Angriffsgeschwindigkeit basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5s) -func get_attack_cooldown() -> float: - if equipped_weapon == null: - return 1.5 - return equipped_weapon.attack_speed + 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): @@ -60,29 +322,80 @@ func set_target(new_target, start_attack: bool = false): target = new_target target.show_health() print("Ziel markiert: ", target.name) - if start_attack and can_attack: - autoattack() + if start_attack: + start_autoattack() + if global_cooldown <= 0: + perform_autoattack() -# Autoattack: greift wiederholt an solange Ziel gültig ist -func 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 - return - if not can_attack: + autoattack_active = false return var distance = global_position.distance_to(target.global_position) if distance <= get_attack_range(): - can_attack = false var dmg = get_attack_damage() - target.take_damage(dmg) - print("Autoattack: ", dmg, " Schaden") - await get_tree().create_timer(get_attack_cooldown()).timeout - can_attack = true - autoattack() + # 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: - await get_tree().create_timer(0.5).timeout - autoattack() + 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): @@ -96,8 +409,28 @@ func _try_select_target(start_attack: bool = false): 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 @@ -117,10 +450,12 @@ func _physics_process(delta): # Aktionsleiste 1-9 if Input.is_action_just_pressed("action_1"): hud.set_active_slot(0) - if target != null: - autoattack() + 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"): @@ -140,6 +475,15 @@ func _physics_process(delta): 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"): diff --git a/player.tscn b/player.tscn index 14906d5..4769dc9 100644 --- a/player.tscn +++ b/player.tscn @@ -4,6 +4,8 @@ [ext_resource type="PackedScene" uid="uid://01rrtitc6yh1" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-b.glb" id="2_hqtel"] [ext_resource type="Script" uid="uid://bwtwon54po4w3" path="res://camera_pivot.gd" id="2_onrkg"] [ext_resource type="PackedScene" uid="uid://bej3excyoxrdh" path="res://hud.tscn" id="4_hqtel"] +[ext_resource type="PackedScene" uid="uid://character_panel" path="res://character_panel.tscn" id="5_char_panel"] +[ext_resource type="PackedScene" uid="uid://inventory_panel" path="res://inventory_panel.tscn" id="6_inv_panel"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4flbx"] radius = 0.6 @@ -27,3 +29,7 @@ script = ExtResource("2_onrkg") transform = Transform3D(2, 0, 0, 0, 1.8126155, 0.84523654, 0, -0.84523654, 1.8126155, 0, 5, 5) [node name="HUD" parent="." unique_id=1901284390 instance=ExtResource("4_hqtel")] + +[node name="CharacterPanel" parent="." instance=ExtResource("5_char_panel")] + +[node name="InventoryPanel" parent="." instance=ExtResource("6_inv_panel")] diff --git a/project.godot b/project.godot index b0f0c19..586ae3e 100644 --- a/project.godot +++ b/project.godot @@ -97,6 +97,16 @@ ui_right_mouse={ "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(411, 19),"global_position":Vector2(420, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) ] } +toggle_character={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) +] +} +toggle_inventory={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null) +] +} [physics] diff --git a/world.gd b/world.gd index 3ee514d..053a2fb 100644 --- a/world.gd +++ b/world.gd @@ -2,10 +2,57 @@ # Initialisiert die Spielwelt: weist dem Gegner den Spieler als Ziel zu extends Node3D +const ENEMY_SCENE = preload("res://enemy.tscn") +const CLASS_SELECTION_MENU = preload("res://class_selection_menu.tscn") +const RESPAWN_TIME = 5.0 + +# Startausrüstung +const STARTER_WEAPON = preload("res://equipment/iron_sword.tres") +const STARTER_CHEST = preload("res://equipment/leather_chest.tres") + +@onready var player = $Player + func _ready(): - var player = get_node("Player") + # Klassenauswahl-Menü anzeigen + var menu = CLASS_SELECTION_MENU.instantiate() + add_child(menu) + menu.class_selected.connect(_on_class_selected) + +# Klasse ausgewählt: Spieler initialisieren +func _on_class_selected(character_class: CharacterClass): + player.character_class = character_class + + # Startausrüstung geben + player.equip_item(STARTER_WEAPON) + player.equip_item(STARTER_CHEST) + + player._calculate_stats() + player.current_hp = player.max_hp + player.hud.update_health(player.current_hp, player.max_hp) + print("Klasse gewählt: ", character_class.class_name_de) + + # Jetzt Gegner initialisieren var enemy = get_node("Enemy") + _setup_enemy(enemy) + +# Gegner initialisieren und Signal verbinden +func _setup_enemy(enemy): if enemy and player: enemy.target = player + enemy.enemy_died.connect(_on_enemy_died) else: print("Fehler: Player oder Enemy nicht gefunden!") + +# Gegner gestorben: Nach 5 Sekunden respawnen +func _on_enemy_died(spawn_position: Vector3, _xp_reward: int): + print("Respawn in ", RESPAWN_TIME, " Sekunden...") + await get_tree().create_timer(RESPAWN_TIME).timeout + _spawn_enemy(spawn_position) + +# Neuen Gegner an Position spawnen +func _spawn_enemy(position: Vector3): + var new_enemy = ENEMY_SCENE.instantiate() + add_child(new_enemy) + new_enemy.global_position = position + _setup_enemy(new_enemy) + print("Gegner respawned!")