Magier-Kampfsystem, Castbar, Inventar-Drag und Gegner-Aggro

- Zauberstab als eigener Fernkampf-Skill (20m, magisch, exklusiv mit Autoattack)
- Frostblitz mit 1.5s Castzeit und Castbar (mittig über Aktionsleiste)
- Cast wird durch Bewegung, Springen oder Schaden unterbrochen
- Holzstab als Magier-Startwaffe (+3 INT)
- Frostblitz-Icon (SVG)
- Skills klassenabhängig: Magier=Zauberstab+Frostblitz, Krieger/Schurke=Heavy Strike
- Inventar: Drag & Drop zum Umordnen mit gelbem Highlight
- Gegner aggrot sofort bei Schadenstreffer (nicht nur in Aggro-Range)
- Inventar: swap_items/move_items Funktionen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andre 2026-03-15 21:41:14 +01:00
parent d029a37e7f
commit 8f0ac2227e
13 changed files with 574 additions and 41 deletions

View file

@ -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

View file

@ -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()

View file

@ -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")

92
hud.gd
View file

@ -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

14
icons/frostbolt_icon.svg Normal file
View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Eisstrahl -->
<polygon points="10,32 32,24 54,32 32,40" fill="#66CCFF" stroke="#3399DD" stroke-width="1.5"/>
<!-- Eiskristall-Spitze -->
<polygon points="54,32 62,32 58,26 58,38" fill="#99DDFF" stroke="#3399DD" stroke-width="1"/>
<!-- Frost-Partikel -->
<circle cx="20" cy="28" r="3" fill="white" opacity="0.7"/>
<circle cx="38" cy="36" r="2.5" fill="white" opacity="0.6"/>
<circle cx="28" cy="22" r="2" fill="white" opacity="0.5"/>
<!-- Schneeflocke Mitte -->
<line x1="32" y1="28" x2="32" y2="36" stroke="white" stroke-width="1.5" opacity="0.8"/>
<line x1="28" y1="30" x2="36" y2="34" stroke="white" stroke-width="1.5" opacity="0.8"/>
<line x1="28" y1="34" x2="36" y2="30" stroke="white" stroke-width="1.5" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

View file

@ -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

View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Stab -->
<line x1="20" y1="56" x2="38" y2="12" stroke="#8B6914" stroke-width="4" stroke-linecap="round"/>
<!-- Kristall oben -->
<polygon points="38,12 44,6 42,16 48,10" fill="#66CCFF" stroke="#3399DD" stroke-width="1"/>
<polygon points="38,12 32,6 36,16 30,10" fill="#99DDFF" stroke="#3399DD" stroke-width="1"/>
<!-- Glühen -->
<circle cx="38" cy="10" r="6" fill="#66CCFF" opacity="0.3"/>
<circle cx="38" cy="10" r="3" fill="white" opacity="0.5"/>
<!-- Wicklung -->
<line x1="25" y1="44" x2="29" y2="42" stroke="#C0A040" stroke-width="2"/>
<line x1="26" y1="40" x2="30" y2="38" stroke="#C0A040" stroke-width="2"/>
<line x1="27" y1="36" x2="31" y2="34" stroke="#C0A040" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 787 B

View file

@ -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

View file

@ -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()

View file

@ -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:

217
player.gd
View file

@ -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:
# 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()

1
skill_panel.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://gf03d1ewoxcf

View file

@ -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