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