diff --git a/classes/mage.tres b/classes/mage.tres index 160daa3..df179f8 100644 --- a/classes/mage.tres +++ b/classes/mage.tres @@ -16,3 +16,6 @@ strength_per_level = 1.0 agility_per_level = 1.5 intelligence_per_level = 3.0 stamina_per_level = 1.5 +unarmed_min_damage = 2 +unarmed_max_damage = 5 +unarmed_attack_speed = 2.0 diff --git a/enemy.gd b/enemy.gd index 2fc6e7c..4e1ebff 100644 --- a/enemy.gd +++ b/enemy.gd @@ -104,10 +104,15 @@ func show_health(): func hide_health(): health_label.visible = false -# Schaden nehmen und Label aktualisieren (alte Methode für Kompatibilität) +# Schaden nehmen und Label aktualisieren func take_damage(amount): current_hp -= amount _update_label() + # Aggro bei Schaden — sofort angreifen + if current_state == State.PATROL: + current_state = State.CHASE + is_waiting = false + print("Gegner wurde angegriffen und verfolgt den Spieler!") if current_hp <= 0: die() diff --git a/equipment/wooden_staff.tres b/equipment/wooden_staff.tres new file mode 100644 index 0000000..d099b83 --- /dev/null +++ b/equipment/wooden_staff.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="Equipment" format=3 uid="uid://wooden_staff_01"] + +[ext_resource type="Script" uid="uid://re0xiie1udfq" path="res://equipment.gd" id="1_equipment"] +[ext_resource type="Texture2D" path="res://icons/wooden_staff_icon.svg" id="2_icon"] + +[resource] +script = ExtResource("1_equipment") +item_name = "Holzstab" +intelligence = 3 +min_damage = 2 +max_damage = 5 +attack_speed = 2.0 +weapon_range = 3.0 +icon = ExtResource("2_icon") diff --git a/hud.gd b/hud.gd index cd8afa2..f0b0ac4 100644 --- a/hud.gd +++ b/hud.gd @@ -42,6 +42,11 @@ var slot_stack_labels = [] # Label für Stack-Anzahl var resource_bar: ProgressBar var resource_label: Label +# Castbar +var castbar: ProgressBar +var castbar_label: Label +var castbar_container: PanelContainer + func _ready(): _create_level_ui() @@ -147,10 +152,10 @@ func _input(event): func _end_actionbar_drag(): var drop_slot = get_slot_at_position(get_viewport().get_mouse_position()) - if drop_slot >= 2 and drop_slot <= 8 and drop_slot != drag_from_slot: + if drop_slot >= 0 and drop_slot <= 8 and drop_slot != drag_from_slot: # Auf anderen Slot verschoben -> swap slot_drag_swapped.emit(drag_from_slot, drop_slot) - elif drop_slot < 0 or drop_slot < 2 or drop_slot > 8: + elif drop_slot < 0 or drop_slot > 8: # Außerhalb gedroppt -> aus Leiste entfernen slot_drag_removed.emit(drag_from_slot) # Aufräumen @@ -253,6 +258,9 @@ func _create_level_ui(): gold_label.text = "0 Gold" control.add_child(gold_label) + # Castbar — direkt über der Aktionsleiste positioniert + _create_castbar() + # Gold aktualisieren func update_gold(amount: int): if gold_label: @@ -340,7 +348,7 @@ func update_drag_hover(mouse_pos: Vector2): # Alten Highlight entfernen _clear_drag_highlight() # Neuen Highlight setzen (nur Slots 2-8) - if hovered >= 2 and hovered <= 8: + if hovered >= 0 and hovered <= 8: drag_highlight_slot = hovered var style = StyleBoxFlat.new() style.bg_color = Color(0.15, 0.15, 0.15) @@ -361,3 +369,81 @@ func get_slot_at_position(mouse_pos: Vector2) -> int: if rect.has_point(mouse_pos): return i return -1 + +func _create_castbar(): + # Eigene CanvasLayer für die Castbar + var castbar_layer = CanvasLayer.new() + castbar_layer.name = "CastbarLayer" + castbar_layer.layer = 10 + add_child(castbar_layer) + + castbar_container = PanelContainer.new() + castbar_container.name = "CastbarContainer" + castbar_container.visible = false + castbar_container.mouse_filter = Control.MOUSE_FILTER_IGNORE + + var style = StyleBoxFlat.new() + style.bg_color = Color(0.1, 0.1, 0.1, 0.85) + style.set_border_width_all(2) + style.border_color = Color(0.6, 0.6, 0.6) + style.set_corner_radius_all(4) + castbar_container.add_theme_stylebox_override("panel", style) + + var vbox = VBoxContainer.new() + vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + castbar_container.add_child(vbox) + + castbar_label = Label.new() + castbar_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + castbar_label.add_theme_font_size_override("font_size", 13) + castbar_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + vbox.add_child(castbar_label) + + castbar = ProgressBar.new() + castbar.custom_minimum_size = Vector2(300, 16) + castbar.show_percentage = false + castbar.mouse_filter = Control.MOUSE_FILTER_IGNORE + + var fill = StyleBoxFlat.new() + fill.bg_color = Color(1.0, 0.7, 0.0, 1.0) + fill.set_corner_radius_all(2) + castbar.add_theme_stylebox_override("fill", fill) + + var bg = StyleBoxFlat.new() + bg.bg_color = Color(0.2, 0.2, 0.2, 1.0) + bg.set_corner_radius_all(2) + castbar.add_theme_stylebox_override("background", bg) + + vbox.add_child(castbar) + castbar_layer.add_child(castbar_container) + +# Castbar anzeigen +func show_castbar(spell_name: String, cast_time: float): + if castbar_container == null: + return + # Spell-Name übersetzen + var display_name = spell_name + match spell_name: + "frostbolt": display_name = "Frostblitz" + castbar_label.text = display_name + " (" + "%.1f" % cast_time + "s)" + castbar.max_value = cast_time + castbar.value = 0 + # Mittig über der Aktionsleiste positionieren + var viewport_size = get_viewport().get_visible_rect().size + castbar_container.position = Vector2(viewport_size.x / 2 - 160, viewport_size.y - 120) + castbar_container.visible = true + +# Castbar Fortschritt aktualisieren +func update_castbar(elapsed: float, total: float): + if castbar == null: + return + castbar.value = elapsed + var remaining = total - elapsed + if remaining < 0: + remaining = 0 + castbar_label.text = castbar_label.text.split("(")[0].strip_edges() + " (%.1f" % remaining + "s)" + +# Castbar verstecken +func hide_castbar(): + if castbar_container: + castbar_container.visible = false diff --git a/icons/frostbolt_icon.svg b/icons/frostbolt_icon.svg new file mode 100644 index 0000000..ecde089 --- /dev/null +++ b/icons/frostbolt_icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/icons/frostbolt_icon.svg.import b/icons/frostbolt_icon.svg.import new file mode 100644 index 0000000..c88df9f --- /dev/null +++ b/icons/frostbolt_icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3lnef1xxgol3" +path="res://.godot/imported/frostbolt_icon.svg-97e9c6299dcf36b8065da5f6427efafe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icons/frostbolt_icon.svg" +dest_files=["res://.godot/imported/frostbolt_icon.svg-97e9c6299dcf36b8065da5f6427efafe.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/icons/wooden_staff_icon.svg b/icons/wooden_staff_icon.svg new file mode 100644 index 0000000..e1d59f4 --- /dev/null +++ b/icons/wooden_staff_icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/icons/wooden_staff_icon.svg.import b/icons/wooden_staff_icon.svg.import new file mode 100644 index 0000000..fb43f97 --- /dev/null +++ b/icons/wooden_staff_icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dts32a6fpdc84" +path="res://.godot/imported/wooden_staff_icon.svg-9178c7c24ac6ded32a37ea646c50397f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icons/wooden_staff_icon.svg" +dest_files=["res://.godot/imported/wooden_staff_icon.svg-9178c7c24ac6ded32a37ea646c50397f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/inventory.gd b/inventory.gd index 8e35dc2..f39a816 100644 --- a/inventory.gd +++ b/inventory.gd @@ -90,6 +90,39 @@ func get_item(index: int): return null return items[index] +# Zwei Items im Inventar tauschen +func swap_items(index_a: int, index_b: int): + if index_a < 0 or index_b < 0: + return + if index_a >= items.size() or index_b >= items.size(): + return + var temp = items[index_a] + items[index_a] = items[index_b] + items[index_b] = temp + inventory_changed.emit() + +# Item an bestimmten Index verschieben (von aktuellem Platz) +func move_item(from_index: int, to_index: int): + if from_index < 0 or from_index >= items.size(): + return + if to_index < 0 or to_index >= MAX_SLOTS: + return + if from_index == to_index: + return + if to_index < items.size(): + # Ziel belegt -> tauschen + swap_items(from_index, to_index) + else: + # Ziel leer -> Item verschieben + var item = items[from_index] + items.remove_at(from_index) + # Index anpassen falls nötig + if to_index >= items.size(): + items.append(item) + else: + items.insert(to_index, item) + inventory_changed.emit() + # Anzahl Items func item_count() -> int: return items.size() diff --git a/inventory_panel.gd b/inventory_panel.gd index fc31462..e274575 100644 --- a/inventory_panel.gd +++ b/inventory_panel.gd @@ -9,8 +9,10 @@ var player = null # Drag & Drop var dragging = false -var drag_item: Consumable = null +var drag_item = null # Equipment oder Consumable +var drag_from_index: int = -1 # Inventar-Index von dem gedraggt wird var drag_icon: TextureRect = null +var drag_highlight_slot: int = -1 # Aktuell hervorgehobener Inventar-Slot @onready var panel = $Panel @onready var gold_label = $Panel/VBoxContainer/Header/GoldLabel @@ -68,6 +70,9 @@ func _create_slot(index: int) -> Panel: style.set_border_width_all(1) slot.add_theme_stylebox_override("panel", style) + # Alle Slots brauchen gui_input für Drop-Erkennung + slot.gui_input.connect(_on_slot_input.bind(index)) + # Item vorhanden? if player.inventory and index < player.inventory.item_count(): var item = player.inventory.get_item(index) @@ -85,6 +90,7 @@ func _create_slot(index: int) -> Panel: icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.custom_minimum_size = Vector2(SLOT_SIZE - 4, SLOT_SIZE - 4) icon.position = Vector2(2, 2) + icon.mouse_filter = Control.MOUSE_FILTER_IGNORE slot.add_child(icon) else: # Fallback: Text @@ -96,6 +102,7 @@ func _create_slot(index: int) -> Panel: if item is Equipment: label.modulate = Equipment.get_rarity_color(item.rarity) label.anchors_preset = Control.PRESET_FULL_RECT + label.mouse_filter = Control.MOUSE_FILTER_IGNORE slot.add_child(label) # Stack-Count für Consumables @@ -107,25 +114,30 @@ func _create_slot(index: int) -> Panel: stack_label.add_theme_font_size_override("font_size", 11) stack_label.size = Vector2(SLOT_SIZE - 4, SLOT_SIZE - 4) stack_label.position = Vector2(0, 0) + stack_label.mouse_filter = Control.MOUSE_FILTER_IGNORE slot.add_child(stack_label) # Rahmen if item is Equipment: style.border_color = Equipment.get_rarity_color(item.rarity) elif item is Consumable: - style.border_color = Color(0.3, 0.7, 0.3) # Grüner Rand für Consumables + style.border_color = Color(0.3, 0.7, 0.3) style.set_border_width_all(2) - # Klick-Handler - slot.gui_input.connect(_on_slot_clicked.bind(index, item)) - # Tooltip slot.tooltip_text = _get_item_tooltip(item) return slot -func _on_slot_clicked(event: InputEvent, index: int, item): +func _on_slot_input(event: InputEvent, index: int): if event is InputEventMouseButton and event.pressed: + var item = null + if player.inventory and index < player.inventory.item_count(): + item = player.inventory.get_item(index) + + if item == null: + return + if item is Equipment: if event.button_index == MOUSE_BUTTON_RIGHT: # Rechtsklick auf Equipment: Anlegen @@ -135,6 +147,9 @@ func _on_slot_clicked(event: InputEvent, index: int, item): if old_item: player.inventory.add_item(old_item) _refresh_inventory() + elif event.button_index == MOUSE_BUTTON_LEFT: + # Linksklick: Drag starten + _start_drag(item, index) elif item is Consumable: if event.button_index == MOUSE_BUTTON_RIGHT: # Rechtsklick auf Consumable: Direkt benutzen @@ -146,15 +161,18 @@ func _on_slot_clicked(event: InputEvent, index: int, item): player._update_action_bar_stacks() elif event.button_index == MOUSE_BUTTON_LEFT: # Linksklick: Drag starten - _start_drag(item) + _start_drag(item, index) # Drag & Drop System -func _start_drag(item: Consumable): +func _start_drag(item, index: int): dragging = true drag_item = item + drag_from_index = index # Icon am Mauszeiger erstellen + var tex = item.icon if item.icon else null drag_icon = TextureRect.new() - drag_icon.texture = item.icon + if tex: + drag_icon.texture = tex drag_icon.custom_minimum_size = Vector2(40, 40) drag_icon.size = Vector2(40, 40) drag_icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL @@ -174,9 +192,12 @@ func _start_drag(item: Consumable): func _process(_delta): if dragging and drag_icon: drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20) + var mouse_pos = get_viewport().get_mouse_position() # HUD Slots highlighten if player and player.hud: - player.hud.update_drag_hover(get_viewport().get_mouse_position()) + player.hud.update_drag_hover(mouse_pos) + # Inventar-Slots highlighten + _update_inventory_hover(mouse_pos) func _input(event): if not dragging: @@ -188,20 +209,80 @@ func _input(event): func _end_drag(): if not dragging: return - # Prüfen ob über einem Action-Slot + var mouse_pos = get_viewport().get_mouse_position() + + # Prüfen ob über einem Inventar-Slot + var inv_slot = _get_inventory_slot_at(mouse_pos) + if inv_slot >= 0 and inv_slot != drag_from_index: + # Innerhalb Inventar verschieben/tauschen + player.inventory.move_item(drag_from_index, inv_slot) + else: + # Prüfen ob über einem Action-Slot (nur Consumables) + if player and player.hud: + var action_slot = player.hud.get_slot_at_position(mouse_pos) + if action_slot >= 0 and action_slot <= 8 and drag_item: + if drag_item is Consumable: + player.assign_to_action_bar(action_slot, drag_item) + print(drag_item.item_name + " auf Aktionsleiste Slot " + str(action_slot + 1) + " gelegt") + + # HUD Highlight deaktivieren if player and player.hud: - var slot_index = player.hud.get_slot_at_position(get_viewport().get_mouse_position()) - if slot_index >= 2 and slot_index <= 8 and drag_item: - player.assign_to_action_bar(slot_index, drag_item) - print(drag_item.item_name + " auf Slot " + str(slot_index + 1) + " gelegt") player.hud.set_drag_active(false) + # Inventar Highlight entfernen + _clear_inventory_highlight() # Aufräumen if drag_icon: var drag_layer = drag_icon.get_parent() - drag_layer.queue_free() # Entfernt DragLayer + Icon + drag_layer.queue_free() drag_icon = null dragging = false drag_item = null + drag_from_index = -1 + +# Inventar-Slot unter Mausposition finden +func _get_inventory_slot_at(mouse_pos: Vector2) -> int: + for i in range(item_grid.get_child_count()): + var slot = item_grid.get_child(i) + var rect = slot.get_global_rect() + if rect.has_point(mouse_pos): + return i + return -1 + +# Inventar-Slot Highlight während Drag +func _update_inventory_hover(mouse_pos: Vector2): + var hovered = _get_inventory_slot_at(mouse_pos) + if hovered == drag_highlight_slot: + return + _clear_inventory_highlight() + if hovered >= 0 and hovered != drag_from_index: + drag_highlight_slot = hovered + var slot = item_grid.get_child(hovered) + var style = StyleBoxFlat.new() + style.bg_color = Color(0.15, 0.15, 0.15) + style.border_color = Color(1, 0.9, 0, 1) # Gelber Rand + style.set_border_width_all(3) + slot.add_theme_stylebox_override("panel", style) + +func _clear_inventory_highlight(): + if drag_highlight_slot >= 0 and drag_highlight_slot < item_grid.get_child_count(): + # Style zurücksetzen + var slot = item_grid.get_child(drag_highlight_slot) + var item = null + if player.inventory and drag_highlight_slot < player.inventory.item_count(): + item = player.inventory.get_item(drag_highlight_slot) + var style = StyleBoxFlat.new() + style.bg_color = Color(0.15, 0.15, 0.15) + if item is Equipment: + style.border_color = Equipment.get_rarity_color(item.rarity) + style.set_border_width_all(2) + elif item is Consumable: + style.border_color = Color(0.3, 0.7, 0.3) + style.set_border_width_all(2) + else: + style.border_color = Color(0.3, 0.3, 0.3) + style.set_border_width_all(1) + slot.add_theme_stylebox_override("panel", style) + drag_highlight_slot = -1 func _get_item_tooltip(item) -> String: if item is Consumable: @@ -217,7 +298,7 @@ func _get_consumable_tooltip(item: Consumable) -> String: tooltip += item.get_effect_text() + "\n" tooltip += "Cooldown: " + str(item.cooldown) + "s\n" tooltip += "Anzahl: " + str(item.stack_size) + "/" + str(item.max_stack) + "\n" - tooltip += "\n[Rechtsklick: Benutzen]\n[Shift+Linksklick: Auf Leiste legen]" + tooltip += "\n[Rechtsklick: Benutzen]\n[Linksklick: Ziehen]" return tooltip func _get_equipment_tooltip(item: Equipment) -> String: diff --git a/player.gd b/player.gd index 62db527..542fd4b 100644 --- a/player.gd +++ b/player.gd @@ -30,13 +30,19 @@ var current_resource = 0 var target = null # Aktuell markierter Gegner # Aktionsleiste: Skills (String) oder Consumables in Slots (0-8) -# Skills: "autoattack", "heavy_strike" — frei verschiebbar -var action_bar_items: Array = ["autoattack", "heavy_strike", null, null, null, null, null, null, null] +var action_bar_items: Array = [null, null, null, null, null, null, null, null, null] -# Alle verfügbaren Skills (für Fähigkeiten-Panel) -var available_skills: Array = [ - {"id": "autoattack", "name": "Autoattack", "icon": "res://icons/autoattack_icon.svg", "description": "Greift das Ziel automatisch an.\nSchaden: Waffenschaden + Main-Stat"}, - {"id": "heavy_strike", "name": "Heavy Strike", "icon": "res://icons/heavy_strike_icon.svg", "description": "Starker Hieb mit 3s Cooldown.\nSchaden: 10-15 + Main-Stat"}, +# 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/autoattack_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 @@ -63,6 +69,10 @@ 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 @@ -70,6 +80,20 @@ 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 + @onready var camera_pivot = $CameraPivot @onready var camera = $CameraPivot/Camera3D @onready var hud = $HUD @@ -88,6 +112,8 @@ func _ready(): 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) @@ -138,6 +164,21 @@ func _calculate_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: @@ -304,8 +345,12 @@ func _get_slot_cooldown(slot_index: int) -> float: 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 @@ -334,10 +379,18 @@ func _use_action_slot(slot_index: int): 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: @@ -348,6 +401,8 @@ func _use_action_slot(slot_index: int): 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() @@ -488,7 +543,7 @@ func get_dps() -> float: # DPS = Schaden / GCD return total_damage / gcd -# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 3.0) +# Reichweite basierend auf ausgerüsteter Waffe (Nahkampf) func get_attack_range() -> float: var weapon = get_equipped_weapon() if weapon == null: @@ -503,9 +558,7 @@ func set_target(new_target, start_attack: bool = false): target.show_health() print("Ziel markiert: ", target.name) if start_attack: - start_autoattack() - if global_cooldown <= 0: - perform_autoattack() + _start_default_attack() # Ziel komplett aufheben und Autoattack stoppen func clear_target(): @@ -513,7 +566,19 @@ func clear_target(): target.hide_health() target = null autoattack_active = false - print("Ziel aufgehoben, Autoattack gestoppt") + 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(): @@ -525,6 +590,34 @@ 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") + 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): @@ -535,13 +628,11 @@ func perform_autoattack(): 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 + 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()]) - # GCD auslösen basierend auf Waffengeschwindigkeit + Haste trigger_global_cooldown() # Global Cooldown auslösen (basierend auf Waffe + Haste) @@ -577,6 +668,82 @@ func use_heavy_strike(): 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 @@ -595,17 +762,29 @@ func _try_select_target(start_attack: bool = false): 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 Autoattack aktiv, versuche anzugreifen - if global_cooldown <= 0 and autoattack_active: - perform_autoattack() + # 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 @@ -621,6 +800,8 @@ func _physics_process(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"): @@ -665,6 +846,10 @@ func _physics_process(delta): 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() diff --git a/skill_panel.gd.uid b/skill_panel.gd.uid new file mode 100644 index 0000000..6ee7b18 --- /dev/null +++ b/skill_panel.gd.uid @@ -0,0 +1 @@ +uid://gf03d1ewoxcf diff --git a/world.gd b/world.gd index 2c5e143..bd28344 100644 --- a/world.gd +++ b/world.gd @@ -8,7 +8,8 @@ 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_SWORD = preload("res://equipment/iron_sword.tres") +const STARTER_STAFF = preload("res://equipment/wooden_staff.tres") const STARTER_CHEST = preload("res://equipment/leather_chest.tres") # Loot Tables @@ -34,13 +35,23 @@ func _on_start_game(): func _on_class_selected(character_class: CharacterClass): player.character_class = character_class - # Startausrüstung geben - player.equip_item(STARTER_WEAPON) + # Skills klassenabhängig aufbauen + player._init_class_skills() + for i in range(9): + player._refresh_action_slot(i) + + # Startausrüstung klassenabhängig + if character_class.resource_type == CharacterClass.ResourceType.MANA: + player.equip_item(STARTER_STAFF) + else: + player.equip_item(STARTER_SWORD) player.equip_item(STARTER_CHEST) player._calculate_stats() player.current_hp = player.max_hp + player.current_resource = player.max_resource player.hud.update_health(player.current_hp, player.max_hp) + player.hud.update_resource(player.current_resource, player.max_resource, player.get_resource_name()) print("Klasse gewählt: ", character_class.class_name_de) # Jetzt Gegner initialisieren