inventory angefangen

This commit is contained in:
Andre 2026-03-15 00:38:53 +01:00
parent 04f22183ce
commit 1616431d1c
8 changed files with 749 additions and 59 deletions

View file

@ -23,7 +23,9 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge
| RMB gehalten | Kamera drehen, Spieler schaut mit | | RMB gehalten | Kamera drehen, Spieler schaut mit |
| Linksklick auf Gegner | Ziel markieren | | Linksklick auf Gegner | Ziel markieren |
| Rechtsklick auf Gegner | Ziel markieren + Autoattack starten | | Rechtsklick auf Gegner | Ziel markieren + Autoattack starten |
| 1 9 | Aktionsleiste Slot auswählen | | 1 | Autoattack manuell starten |
| 2 | Heavy Strike (starke Attacke) |
| 3 9 | Aktionsleiste Slots (noch frei) |
| Leertaste | Springen | | Leertaste | Springen |
| T | (Test) 10 Schaden am Spieler | | T | (Test) 10 Schaden am Spieler |
@ -198,6 +200,31 @@ Definiert einen Angriff — Schaden kommt von der ausgerüsteten Waffe.
- Ohne Waffe: 1 Schaden, 1.5s Cooldown, 1.5 Reichweite - Ohne Waffe: 1 Schaden, 1.5s Cooldown, 1.5 Reichweite
- Mit Waffe: Schaden = zufällig zwischen min/max, Speed und Range von der Waffe - Mit Waffe: Schaden = zufällig zwischen min/max, Speed und Range von der Waffe
### Skills
#### Autoattack (Taste 1)
- **Schaden**: 1 (unbewaffnet) oder Waffenschaden
- **Cooldown**: 1.5s (unbewaffnet) oder Waffengeschwindigkeit
- **Reichweite**: 1.5 (unbewaffnet) oder Waffenreichweite
- **Icon**: Faust mit grünem Kreispfeil
- Manuell per Taste 1 starten oder automatisch per Rechtsklick
#### Heavy Strike (Taste 2)
- **Schaden**: 10-15 (zufällig)
- **Cooldown**: 3 Sekunden
- **Reichweite**: 2.0
- **Icon**: Schwert mit roten Schlaglinien
- Manuell aktivierbare starke Attacke für höheren Schaden
### UI & Icons
- Aktionsleiste zeigt Icons für Skills an
- Icons werden dynamisch beim Spielstart geladen
- Slot 1: Autoattack Icon (Faust)
- Slot 2: Heavy Strike Icon (Schwert)
- Slots 3-9: Verfügbar für weitere Skills
- **Mausklick**: Alle Slots sind per Maus klickbar
- **Cooldown-Anzeige**: Dunkle Überlagerung mit verbleibender Zeit in Sekunden
### Schadenstypen (geplant) ### Schadenstypen (geplant)
- **PHYSICAL** normaler Schaden - **PHYSICAL** normaler Schaden
- **FIRE** Feuerschaden (z.B. Magier) - **FIRE** Feuerschaden (z.B. Magier)

182
enemy.gd
View file

@ -2,26 +2,95 @@
# Steuert den Gegner: KI-Bewegung zum Spieler, Angriff, HP, Zielanzeige # Steuert den Gegner: KI-Bewegung zum Spieler, Angriff, HP, Zielanzeige
extends CharacterBody3D extends CharacterBody3D
signal enemy_died(spawn_position: Vector3, xp_reward: int)
const SPEED = 3.0 const SPEED = 3.0
const PATROL_SPEED = 1.5
const GRAVITY = 9.8 const GRAVITY = 9.8
const ATTACK_DAMAGE = 5
const ATTACK_RANGE = 1.5 const ATTACK_RANGE = 1.5
const ATTACK_COOLDOWN = 2.0 const ATTACK_COOLDOWN = 2.0
const AGGRO_RANGE = 8.0 # Entfernung ab der der Gegner angreift
const PATROL_RADIUS = 5.0 # Radius um Spawn-Position für Patrol
const PATROL_WAIT_TIME = 2.0 # Wartezeit am Patrol-Punkt
var max_hp = 50 # Level-Differenz Konstanten
var current_hp = 50 const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz
var target = null # Ziel des Gegners (normalerweise der Spieler) const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation
enum State { PATROL, CHASE, ATTACK }
# Stats-System
@export var level: int = 1
@export var base_strength: int = 8
@export var base_stamina: int = 10
@export var base_armor: int = 5 # Rüstung reduziert Nahkampfschaden
# Berechnete Stats
var strength: int = 8
var stamina: int = 10
var armor: int = 5
var max_hp: int = 100
var current_hp: int = 100
var attack_damage: int = 5
# XP-Belohnung (skaliert mit Level)
var xp_reward: int = 25
var target = null # Spieler-Referenz (wird von World gesetzt)
var can_attack = true var can_attack = true
var spawn_position: Vector3 # Ursprüngliche Spawn-Position
var current_state = State.PATROL
var patrol_target: Vector3 # Aktuelles Patrol-Ziel
var is_waiting = false # Ob Gegner am Patrol-Punkt wartet
@onready var health_label = $HealthLabel @onready var health_label = $HealthLabel
func _ready(): func _ready():
_calculate_stats()
current_hp = max_hp
health_label.visible = false health_label.visible = false
_update_label() _update_label()
spawn_position = global_position
_pick_new_patrol_target()
# Stats basierend auf Level berechnen
func _calculate_stats():
var levels_gained = level - 1
strength = base_strength + levels_gained * 2
stamina = base_stamina + levels_gained * 3
armor = base_armor + levels_gained * 2
# HP = Stamina * 10
max_hp = stamina * 10
# Schaden = Stärke / 2
attack_damage = int(strength * 0.5) + 2
# XP = 25 * Level
xp_reward = 25 * level
print("Enemy Stats (Lv", level, ") - STR:", strength, " STA:", stamina, " ARM:", armor, " HP:", max_hp, " DMG:", attack_damage)
# Schaden mit Rüstung und Level-Differenz berechnen
func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int:
var damage = float(raw_damage)
# Rüstung reduziert nur Nahkampfschaden
if is_melee:
# Rüstungsreduktion: armor / (armor + 50) = Prozent Reduktion
# Bei 5 Rüstung: 5/55 = ~9% Reduktion
# Bei 20 Rüstung: 20/70 = ~29% Reduktion
var armor_reduction = float(armor) / (float(armor) + 50.0)
damage = damage * (1.0 - armor_reduction)
# Level-Differenz Modifikator
var level_diff = attacker_level - level
var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD)
damage = damage * (1.0 + level_mod)
return maxi(1, int(damage)) # Mindestens 1 Schaden
# HP-Label Text aktualisieren # HP-Label Text aktualisieren
func _update_label(): func _update_label():
health_label.text = str(current_hp) + " / " + str(max_hp) health_label.text = "Lv" + str(level) + " " + str(current_hp) + "/" + str(max_hp)
# HP-Label anzeigen (wenn Gegner markiert wird) # HP-Label anzeigen (wenn Gegner markiert wird)
func show_health(): func show_health():
@ -31,16 +100,26 @@ func show_health():
func hide_health(): func hide_health():
health_label.visible = false health_label.visible = false
# Schaden nehmen und Label aktualisieren # Schaden nehmen und Label aktualisieren (alte Methode für Kompatibilität)
func take_damage(amount): func take_damage(amount):
current_hp -= amount current_hp -= amount
_update_label() _update_label()
if current_hp <= 0: if current_hp <= 0:
die() die()
# Schaden mit vollem Schadenssystem (Rüstung, Level-Differenz)
func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true):
var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee)
print("Eingehender Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)")
take_damage(final_damage)
# Gegner aus der Szene entfernen # Gegner aus der Szene entfernen
func die(): func die():
print("Gegner besiegt!") print("Gegner besiegt! +", xp_reward, " XP")
# XP an Spieler geben
if target and target.has_method("gain_xp"):
target.gain_xp(xp_reward)
enemy_died.emit(spawn_position, xp_reward)
queue_free() queue_free()
func _physics_process(delta): func _physics_process(delta):
@ -51,29 +130,84 @@ func _physics_process(delta):
move_and_slide() move_and_slide()
return return
var distance = global_position.distance_to(target.global_position) # Prüfe Distanz zum Spieler für Aggro
var distance_to_player = global_position.distance_to(target.global_position)
if distance <= ATTACK_RANGE: # State-Wechsel basierend auf Distanz
# In Reichweite: angreifen match current_state:
velocity.x = 0 State.PATROL:
velocity.z = 0 if distance_to_player <= AGGRO_RANGE:
if can_attack: current_state = State.CHASE
_attack() print("Gegner hat Spieler entdeckt!")
else: else:
# Direkt auf Ziel zubewegen _do_patrol()
var direction = (target.global_position - global_position) State.CHASE:
direction.y = 0 if distance_to_player <= ATTACK_RANGE:
direction = direction.normalized() current_state = State.ATTACK
velocity.x = direction.x * SPEED else:
velocity.z = direction.z * SPEED _chase_player()
look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z)) State.ATTACK:
if distance_to_player > ATTACK_RANGE:
current_state = State.CHASE
else:
velocity.x = 0
velocity.z = 0
if can_attack:
_attack()
move_and_slide() move_and_slide()
# Neues Patrol-Ziel in der Nähe der Spawn-Position wählen
func _pick_new_patrol_target():
var angle = randf() * TAU # Zufälliger Winkel
var distance = randf_range(2.0, PATROL_RADIUS)
patrol_target = spawn_position + Vector3(cos(angle) * distance, 0, sin(angle) * distance)
# Patrol-Verhalten: Zufällig herumlaufen
func _do_patrol():
if is_waiting:
return
var distance_to_patrol = global_position.distance_to(patrol_target)
if distance_to_patrol <= 0.5:
# Am Ziel angekommen, warten und neues Ziel wählen
velocity.x = 0
velocity.z = 0
_wait_at_patrol_point()
else:
# Zum Patrol-Ziel laufen
var direction = (patrol_target - global_position)
direction.y = 0
direction = direction.normalized()
velocity.x = direction.x * PATROL_SPEED
velocity.z = direction.z * PATROL_SPEED
look_at(Vector3(patrol_target.x, global_position.y, patrol_target.z))
# Am Patrol-Punkt warten
func _wait_at_patrol_point():
is_waiting = true
await get_tree().create_timer(PATROL_WAIT_TIME).timeout
is_waiting = false
_pick_new_patrol_target()
# Spieler verfolgen
func _chase_player():
var direction = (target.global_position - global_position)
direction.y = 0
direction = direction.normalized()
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z))
# Angriff mit Cooldown # Angriff mit Cooldown
func _attack(): func _attack():
can_attack = false can_attack = false
target.take_damage(ATTACK_DAMAGE) # Gegner verwendet auch das Schadenssystem mit Level-Differenz
print("Gegner greift an!") if target.has_method("take_damage_from"):
target.take_damage_from(attack_damage, level, true)
else:
target.take_damage(attack_damage)
print("Gegner (Lv", level, ") greift an: ", attack_damage, " Schaden")
await get_tree().create_timer(ATTACK_COOLDOWN).timeout await get_tree().create_timer(ATTACK_COOLDOWN).timeout
can_attack = true can_attack = true

View file

@ -1,6 +1,7 @@
[gd_scene format=3 uid="uid://cvojaeanxugfj"] [gd_scene format=3 uid="uid://cvojaeanxugfj"]
[ext_resource type="PackedScene" uid="uid://tosl2au4emxt" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-d.glb" id="1_7k104"] [ext_resource type="PackedScene" uid="uid://tosl2au4emxt" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-d.glb" id="1_7k104"]
[ext_resource type="Script" uid="uid://bg5qs3pcfp7p7" path="res://enemy.gd" id="2_enemy"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4gyqm"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4gyqm"]
radius = 0.6 radius = 0.6
@ -11,6 +12,7 @@ radius = 0.6
height = 3.0 height = 3.0
[node name="Enemy" type="CharacterBody3D" unique_id=332011146] [node name="Enemy" type="CharacterBody3D" unique_id=332011146]
script = ExtResource("2_enemy")
[node name="character-d2" parent="." unique_id=846574684 instance=ExtResource("1_7k104")] [node name="character-d2" parent="." unique_id=846574684 instance=ExtResource("1_7k104")]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0) transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0)

122
hud.gd
View file

@ -1,9 +1,15 @@
# HUD.gd # HUD.gd
# Verwaltet die Spieler-UI: HP-Leiste, Aktionsleiste (Slots 1-9) # Verwaltet die Spieler-UI: HP-Leiste, XP-Leiste, Aktionsleiste (Slots 1-9)
extends CanvasLayer extends CanvasLayer
signal slot_clicked(slot_index: int)
@onready var health_bar = $Control/HealthBar @onready var health_bar = $Control/HealthBar
@onready var health_label = $Control/HealthBar/HealthLabel @onready var health_label = $Control/HealthBar/HealthLabel
# Level/XP UI (wird dynamisch erstellt)
var level_label: Label
var xp_bar: ProgressBar
@onready var action_slots = [ @onready var action_slots = [
$Control/ActionBar/A1, $Control/ActionBar/A1,
$Control/ActionBar/A2, $Control/ActionBar/A2,
@ -17,6 +23,112 @@ extends CanvasLayer
] ]
var active_slot = 0 var active_slot = 0
var slot_icons = [] # TextureRect nodes für Icons
var slot_cooldown_overlays = [] # ColorRect für Cooldown-Anzeige
var slot_cooldown_labels = [] # Label für Cooldown-Text
func _ready():
_create_level_ui()
for i in range(9):
# Icon erstellen
var icon = TextureRect.new()
icon.name = "Icon"
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.custom_minimum_size = Vector2(40, 40)
icon.position = Vector2(5, 5)
action_slots[i].add_child(icon)
slot_icons.append(icon)
# Cooldown-Overlay erstellen (dunkle Überlagerung)
var cooldown_overlay = ColorRect.new()
cooldown_overlay.name = "CooldownOverlay"
cooldown_overlay.color = Color(0, 0, 0, 0.7)
cooldown_overlay.size = Vector2(50, 50)
cooldown_overlay.position = Vector2(0, 0)
cooldown_overlay.visible = false
cooldown_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
action_slots[i].add_child(cooldown_overlay)
slot_cooldown_overlays.append(cooldown_overlay)
# Cooldown-Text erstellen
var cooldown_label = Label.new()
cooldown_label.name = "CooldownLabel"
cooldown_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
cooldown_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
cooldown_label.size = Vector2(50, 50)
cooldown_label.position = Vector2(0, 0)
cooldown_label.add_theme_font_size_override("font_size", 16)
cooldown_label.visible = false
cooldown_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
action_slots[i].add_child(cooldown_label)
slot_cooldown_labels.append(cooldown_label)
# Button für Klicks erstellen
var button = Button.new()
button.name = "SlotButton"
button.flat = true
button.size = Vector2(50, 50)
button.position = Vector2(0, 0)
button.modulate = Color(1, 1, 1, 0) # Unsichtbar
var slot_index = i
button.pressed.connect(func(): _on_slot_clicked(slot_index))
action_slots[i].add_child(button)
# Slot-Klick Handler
func _on_slot_clicked(slot_index: int):
set_active_slot(slot_index)
slot_clicked.emit(slot_index)
# Icon für einen Slot setzen
func set_slot_icon(slot_index: int, icon_path: String):
if slot_index >= 0 and slot_index < 9:
var texture = load(icon_path)
if texture:
slot_icons[slot_index].texture = texture
else:
print("Icon nicht gefunden: ", icon_path)
# Cooldown für einen Slot anzeigen (remaining_time in Sekunden)
func set_slot_cooldown(slot_index: int, remaining_time: float):
if slot_index < 0 or slot_index >= 9:
return
if remaining_time > 0:
slot_cooldown_overlays[slot_index].visible = true
slot_cooldown_labels[slot_index].visible = true
slot_cooldown_labels[slot_index].text = "%.1f" % remaining_time
else:
slot_cooldown_overlays[slot_index].visible = false
slot_cooldown_labels[slot_index].visible = false
# Level/XP UI erstellen
func _create_level_ui():
var control = $Control
# Level Label
level_label = Label.new()
level_label.name = "LevelLabel"
level_label.position = Vector2(20, 55)
level_label.add_theme_font_size_override("font_size", 14)
level_label.text = "Level 1"
control.add_child(level_label)
# XP Bar
xp_bar = ProgressBar.new()
xp_bar.name = "XPBar"
xp_bar.position = Vector2(80, 55)
xp_bar.size = Vector2(140, 18)
xp_bar.show_percentage = false
xp_bar.value = 0
# XP Bar Farbe (blau)
var xp_style = StyleBoxFlat.new()
xp_style.bg_color = Color(0.2, 0.4, 0.9, 1.0)
xp_bar.add_theme_stylebox_override("fill", xp_style)
control.add_child(xp_bar)
# HP-Leiste und Text aktualisieren # HP-Leiste und Text aktualisieren
func update_health(current_hp, max_hp): func update_health(current_hp, max_hp):
@ -24,6 +136,14 @@ func update_health(current_hp, max_hp):
health_bar.value = current_hp health_bar.value = current_hp
health_label.text = str(current_hp) + " / " + str(max_hp) health_label.text = str(current_hp) + " / " + str(max_hp)
# Level und XP aktualisieren
func update_level(level: int, current_xp: int, xp_to_next: int):
if level_label:
level_label.text = "Lv " + str(level)
if xp_bar:
xp_bar.max_value = xp_to_next
xp_bar.value = current_xp
# Aktions-Slot kurz golden hervorheben (0.1s) # Aktions-Slot kurz golden hervorheben (0.1s)
func set_active_slot(index): func set_active_slot(index):
action_slots[active_slot].self_modulate = Color(1, 1, 1) action_slots[active_slot].self_modulate = Color(1, 1, 1)

408
player.gd
View file

@ -6,19 +6,216 @@ const SPEED = 5.0
const JUMP_VELOCITY = 4.5 const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8 const GRAVITY = 9.8
# Charakter-Klasse und Level-System
@export var character_class: CharacterClass
var level: int = 1
var current_xp: int = 0
var xp_to_next_level: int = 100 # XP benötigt für Level 2
# Aktuelle Stats (berechnet aus Klasse + Level)
var strength: int = 10
var agility: int = 10
var intelligence: int = 10
var stamina: int = 10
var armor: int = 0 # Rüstung aus Ausrüstung
# Level-Differenz Konstanten
const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz
const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation
var max_hp = 100 var max_hp = 100
var current_hp = 100 var current_hp = 100
var can_attack = true
var target = null # Aktuell markierter Gegner var target = null # Aktuell markierter Gegner
var equipped_weapon = null # Ausgerüstete Waffe (null = unbewaffnet, Schaden = 1)
# Equipment System
var equipment: Dictionary = {
Equipment.Slot.HEAD: null,
Equipment.Slot.CHEST: null,
Equipment.Slot.HANDS: null,
Equipment.Slot.LEGS: null,
Equipment.Slot.FEET: null,
Equipment.Slot.WEAPON: null,
Equipment.Slot.OFFHAND: null
}
# Inventar System
var inventory: Inventory = Inventory.new()
# Global Cooldown System (GCD) - gilt für alle Aktionen inkl. Autoattack
var global_cooldown = 0.0
const BASE_GCD = 1.5 # Basis-GCD in Sekunden (wird durch Haste modifiziert)
var haste: float = 0.0 # Angriffsgeschwindigkeits-Bonus (0.1 = 10% schneller)
# Autoattack System
var autoattack_active = false # Ob Autoattack aktiv ist
# Skills System - individuelle Cooldowns (zusätzlich zum GCD)
var heavy_strike_cooldown = 0.0
const HEAVY_STRIKE_DAMAGE_MIN = 10
const HEAVY_STRIKE_DAMAGE_MAX = 15
const HEAVY_STRIKE_COOLDOWN = 3.0
const HEAVY_STRIKE_RANGE = 4.0
@onready var camera_pivot = $CameraPivot @onready var camera_pivot = $CameraPivot
@onready var camera = $CameraPivot/Camera3D @onready var camera = $CameraPivot/Camera3D
@onready var hud = $HUD @onready var hud = $HUD
@onready var character_panel = $CharacterPanel
@onready var inventory_panel = $InventoryPanel
func _ready(): func _ready():
# Stats aus Klasse berechnen
_calculate_stats()
current_hp = max_hp
hud.update_health(current_hp, max_hp) hud.update_health(current_hp, max_hp)
hud.update_level(level, current_xp, xp_to_next_level)
hud.set_active_slot(0) hud.set_active_slot(0)
# Icons für Skills setzen
hud.set_slot_icon(0, "res://icons/autoattack_icon.svg") # Slot 1: Autoattack
hud.set_slot_icon(1, "res://icons/heavy_strike_icon.svg") # Slot 2: Heavy Strike
# HUD-Klicks verbinden
hud.slot_clicked.connect(_on_slot_clicked)
# Inventar Panel initialisieren
inventory_panel.setup(self)
# Stats basierend auf Klasse und Level berechnen
func _calculate_stats():
if character_class == null:
# Fallback ohne Klasse
strength = 10
agility = 10
intelligence = 10
stamina = 10
max_hp = 100
return
# Stats = Basis + (Level-1) * Zuwachs pro Level
var levels_gained = level - 1
strength = character_class.base_strength + int(levels_gained * character_class.strength_per_level)
agility = character_class.base_agility + int(levels_gained * character_class.agility_per_level)
intelligence = character_class.base_intelligence + int(levels_gained * character_class.intelligence_per_level)
stamina = character_class.base_stamina + int(levels_gained * character_class.stamina_per_level)
# HP aus Stamina berechnen
max_hp = stamina * CharacterClass.HP_PER_STAMINA
# Equipment-Boni hinzufügen
_apply_equipment_stats()
print("Stats berechnet - STR: ", strength, " AGI: ", agility, " INT: ", intelligence, " STA: ", stamina, " ARM: ", armor, " HP: ", max_hp)
# Equipment-Stats auf Charakter anwenden
func _apply_equipment_stats():
armor = 0
haste = 0.0
var bonus_str = 0
var bonus_agi = 0
var bonus_int = 0
var bonus_sta = 0
for slot in equipment.keys():
var item = equipment[slot]
if item != null:
armor += item.armor
haste += item.haste
bonus_str += item.strength
bonus_agi += item.agility
bonus_int += item.intelligence
bonus_sta += item.stamina
strength += bonus_str
agility += bonus_agi
intelligence += bonus_int
stamina += bonus_sta
# HP neu berechnen mit Equipment-Stamina
max_hp = stamina * CharacterClass.HP_PER_STAMINA
# Equipment anlegen
func equip_item(item: Equipment) -> Equipment:
var old_item = equipment[item.slot]
equipment[item.slot] = item
_calculate_stats()
# HP proportional anpassen
if max_hp > 0:
current_hp = mini(current_hp, max_hp)
hud.update_health(current_hp, max_hp)
character_panel.update_stats(self)
print("Ausgerüstet: ", item.item_name, " in Slot ", Equipment.get_slot_name(item.slot))
return old_item
# Equipment ablegen
func unequip_slot(slot: Equipment.Slot) -> Equipment:
var old_item = equipment[slot]
if old_item == null:
return null
equipment[slot] = null
_calculate_stats()
current_hp = mini(current_hp, max_hp)
hud.update_health(current_hp, max_hp)
character_panel.update_stats(self)
print("Abgelegt: ", old_item.item_name)
return old_item
# Ausgerüstete Waffe holen
func get_equipped_weapon() -> Equipment:
return equipment[Equipment.Slot.WEAPON]
# Main-Stat für Schadensberechnung holen
func get_main_stat() -> int:
if character_class == null:
return 10
match character_class.main_stat:
CharacterClass.MainStat.STRENGTH:
return strength
CharacterClass.MainStat.AGILITY:
return agility
CharacterClass.MainStat.INTELLIGENCE:
return intelligence
return 10
# XP erhalten und Level-Up prüfen
func gain_xp(amount: int):
current_xp += amount
print("+" , amount, " XP (", current_xp, "/", xp_to_next_level, ")")
while current_xp >= xp_to_next_level:
_level_up()
hud.update_level(level, current_xp, xp_to_next_level)
# Level-Up durchführen
func _level_up():
current_xp -= xp_to_next_level
level += 1
xp_to_next_level = _calculate_xp_for_level(level + 1)
# Stats neu berechnen
_calculate_stats()
# HP vollständig auffüllen bei Level-Up
current_hp = max_hp
hud.update_health(current_hp, max_hp)
# Character Panel aktualisieren falls offen
character_panel.update_stats(self)
print("LEVEL UP! Jetzt Level ", level, " - HP voll aufgefüllt!")
# XP-Kurve: Jedes Level braucht mehr XP
func _calculate_xp_for_level(target_level: int) -> int:
return 100 * target_level # Level 2: 100, Level 3: 200, etc.
# Handler für HUD-Slot-Klicks
func _on_slot_clicked(slot_index: int):
match slot_index:
0: # Autoattack manuell starten
if target != null and global_cooldown <= 0:
start_autoattack()
perform_autoattack()
1: # Heavy Strike
use_heavy_strike()
# Schaden am Spieler abziehen und HP-Leiste aktualisieren # Schaden am Spieler abziehen und HP-Leiste aktualisieren
func take_damage(amount): func take_damage(amount):
@ -27,6 +224,28 @@ func take_damage(amount):
if current_hp <= 0: if current_hp <= 0:
die() die()
# Schaden mit Rüstung und Level-Differenz berechnen
func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int:
var damage = float(raw_damage)
# Rüstung reduziert nur Nahkampfschaden
if is_melee and armor > 0:
var armor_reduction = float(armor) / (float(armor) + 50.0)
damage = damage * (1.0 - armor_reduction)
# Level-Differenz Modifikator (Gegner höheres Level = mehr Schaden)
var level_diff = attacker_level - level
var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD)
damage = damage * (1.0 + level_mod)
return maxi(1, int(damage))
# Schaden mit vollem Schadenssystem nehmen
func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true):
var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee)
print("Spieler nimmt Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)")
take_damage(final_damage)
# HP heilen und HP-Leiste aktualisieren # HP heilen und HP-Leiste aktualisieren
func heal(amount): func heal(amount):
current_hp = clamp(current_hp + amount, 0, max_hp) current_hp = clamp(current_hp + amount, 0, max_hp)
@ -35,23 +254,66 @@ func heal(amount):
func die(): func die():
print("Spieler gestorben!") print("Spieler gestorben!")
# Schaden basierend auf ausgerüsteter Waffe (unbewaffnet = 1) # Schaden basierend auf ausgerüsteter Waffe + Main-Stat Skalierung
func get_attack_damage() -> int: func get_attack_damage() -> int:
if equipped_weapon == null: var weapon = get_equipped_weapon()
return 1 var base_damage: int
return randi_range(equipped_weapon.min_damage, equipped_weapon.max_damage) if weapon == null:
# Unbewaffneter Schaden klassenabhängig
if character_class:
base_damage = randi_range(character_class.unarmed_min_damage, character_class.unarmed_max_damage)
else:
base_damage = 1
else:
base_damage = randi_range(weapon.min_damage, weapon.max_damage)
# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5) # Schaden skaliert mit Main-Stat
var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
return base_damage + stat_bonus
# Aktuellen GCD berechnen (mit Haste-Modifikator)
func get_current_gcd() -> float:
var weapon = get_equipped_weapon()
var base_speed: float
if weapon == null:
# Unbewaffnete Angriffsgeschwindigkeit klassenabhängig
if character_class:
base_speed = character_class.unarmed_attack_speed
else:
base_speed = BASE_GCD
else:
base_speed = weapon.attack_speed
# Haste reduziert den GCD: GCD = Basis / (1 + Haste)
# Bei 0.5 Haste (50%): 1.5s / 1.5 = 1.0s
return base_speed / (1.0 + haste)
# DPS berechnen (für Anzeige)
func get_dps() -> float:
var weapon = get_equipped_weapon()
var avg_damage: float
if weapon == null:
# Unbewaffneter Durchschnittsschaden klassenabhängig
if character_class:
avg_damage = (character_class.unarmed_min_damage + character_class.unarmed_max_damage) / 2.0
else:
avg_damage = 1.0
else:
avg_damage = (weapon.min_damage + weapon.max_damage) / 2.0
var stat_bonus = get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT
var total_damage = avg_damage + stat_bonus
var gcd = get_current_gcd()
# DPS = Schaden / GCD
return total_damage / gcd
# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 3.0)
func get_attack_range() -> float: func get_attack_range() -> float:
if equipped_weapon == null: var weapon = get_equipped_weapon()
return 1.5 if weapon == null:
return equipped_weapon.range return 3.0
return weapon.weapon_range
# Angriffsgeschwindigkeit basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5s)
func get_attack_cooldown() -> float:
if equipped_weapon == null:
return 1.5
return equipped_weapon.attack_speed
# Ziel markieren — start_attack=true startet sofort die Autoattack # Ziel markieren — start_attack=true startet sofort die Autoattack
func set_target(new_target, start_attack: bool = false): func set_target(new_target, start_attack: bool = false):
@ -60,29 +322,80 @@ func set_target(new_target, start_attack: bool = false):
target = new_target target = new_target
target.show_health() target.show_health()
print("Ziel markiert: ", target.name) print("Ziel markiert: ", target.name)
if start_attack and can_attack: if start_attack:
autoattack() start_autoattack()
if global_cooldown <= 0:
perform_autoattack()
# Autoattack: greift wiederholt an solange Ziel gültig ist # Ziel komplett aufheben und Autoattack stoppen
func autoattack(): func clear_target():
if target != null and is_instance_valid(target):
target.hide_health()
target = null
autoattack_active = false
print("Ziel aufgehoben, Autoattack gestoppt")
# Autoattack aktivieren
func start_autoattack():
autoattack_active = true
print("Autoattack aktiviert")
# Autoattack deaktivieren
func stop_autoattack():
autoattack_active = false
print("Autoattack deaktiviert")
# Führt einen Autoattack aus (wird vom GCD-System aufgerufen)
func perform_autoattack():
if target == null or not is_instance_valid(target): if target == null or not is_instance_valid(target):
target = null target = null
return autoattack_active = false
if not can_attack:
return return
var distance = global_position.distance_to(target.global_position) var distance = global_position.distance_to(target.global_position)
if distance <= get_attack_range(): if distance <= get_attack_range():
can_attack = false
var dmg = get_attack_damage() var dmg = get_attack_damage()
target.take_damage(dmg) # Neues Schadenssystem mit Rüstung und Level-Differenz
print("Autoattack: ", dmg, " Schaden") if target.has_method("take_damage_from"):
await get_tree().create_timer(get_attack_cooldown()).timeout target.take_damage_from(dmg, level, true) # true = Nahkampf
can_attack = true else:
autoattack() target.take_damage(dmg)
print("Autoattack: ", dmg, " Schaden (GCD: %.2fs, DPS: %.1f)" % [get_current_gcd(), get_dps()])
# GCD auslösen basierend auf Waffengeschwindigkeit + Haste
trigger_global_cooldown()
# Global Cooldown auslösen (basierend auf Waffe + Haste)
func trigger_global_cooldown():
global_cooldown = get_current_gcd()
# Heavy Strike: Starker Angriff mit Cooldown
func use_heavy_strike():
if target == null or not is_instance_valid(target):
print("Kein Ziel für Heavy Strike!")
return
# Nur Skill-eigener Cooldown Check (kein GCD-Check!)
if heavy_strike_cooldown > 0:
print("Heavy Strike noch im Cooldown: ", "%.1f" % heavy_strike_cooldown, "s")
return
var distance = global_position.distance_to(target.global_position)
if distance > HEAVY_STRIKE_RANGE:
print("Ziel zu weit entfernt für Heavy Strike!")
return
var base_damage = randi_range(HEAVY_STRIKE_DAMAGE_MIN, HEAVY_STRIKE_DAMAGE_MAX)
var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
var damage = base_damage + stat_bonus
# Neues Schadenssystem mit Rüstung und Level-Differenz
if target.has_method("take_damage_from"):
target.take_damage_from(damage, level, true) # true = Nahkampf
else: else:
await get_tree().create_timer(0.5).timeout target.take_damage(damage)
autoattack() heavy_strike_cooldown = HEAVY_STRIKE_COOLDOWN
trigger_global_cooldown() # GCD zurücksetzen damit Autoattack nicht sofort feuert
start_autoattack() # Autoattack nach Skill automatisch aktivieren
print("Heavy Strike! ", damage, " Rohschaden")
# Raycast von der Kamera auf Mausposition — trifft Gegner mit take_damage() # Raycast von der Kamera auf Mausposition — trifft Gegner mit take_damage()
func _try_select_target(start_attack: bool = false): func _try_select_target(start_attack: bool = false):
@ -96,8 +409,28 @@ func _try_select_target(start_attack: bool = false):
var result = space_state.intersect_ray(query) var result = space_state.intersect_ray(query)
if result and result.collider.has_method("take_damage"): if result and result.collider.has_method("take_damage"):
set_target(result.collider, start_attack) set_target(result.collider, start_attack)
elif not start_attack:
# Nur bei Linksklick ins Leere: Ziel deselektieren und Autoattack stoppen
# Rechtsklick wird für Kameradrehung verwendet
clear_target()
func _physics_process(delta): func _physics_process(delta):
# Global Cooldown herunterzählen (gilt für alle Aktionen)
if global_cooldown > 0:
global_cooldown -= delta
# Wenn GCD bereit und Autoattack aktiv, versuche anzugreifen
if global_cooldown <= 0 and autoattack_active:
perform_autoattack()
# Skill-Cooldowns herunterzählen
if heavy_strike_cooldown > 0:
heavy_strike_cooldown -= delta
# HUD Cooldowns aktualisieren
hud.set_slot_cooldown(0, global_cooldown) # Slot 1: GCD (Autoattack)
hud.set_slot_cooldown(1, heavy_strike_cooldown) # Slot 2: Heavy Strike CD
# Schwerkraft # Schwerkraft
if not is_on_floor(): if not is_on_floor():
velocity.y -= GRAVITY * delta velocity.y -= GRAVITY * delta
@ -117,10 +450,12 @@ func _physics_process(delta):
# Aktionsleiste 1-9 # Aktionsleiste 1-9
if Input.is_action_just_pressed("action_1"): if Input.is_action_just_pressed("action_1"):
hud.set_active_slot(0) hud.set_active_slot(0)
if target != null: if target != null and global_cooldown <= 0:
autoattack() start_autoattack()
perform_autoattack()
if Input.is_action_just_pressed("action_2"): if Input.is_action_just_pressed("action_2"):
hud.set_active_slot(1) hud.set_active_slot(1)
use_heavy_strike()
if Input.is_action_just_pressed("action_3"): if Input.is_action_just_pressed("action_3"):
hud.set_active_slot(2) hud.set_active_slot(2)
if Input.is_action_just_pressed("action_4"): if Input.is_action_just_pressed("action_4"):
@ -140,6 +475,15 @@ func _physics_process(delta):
if Input.is_action_just_pressed("test_damage"): if Input.is_action_just_pressed("test_damage"):
take_damage(10) take_damage(10)
# C drücken = Charakter-Panel öffnen/schließen
if Input.is_action_just_pressed("toggle_character"):
character_panel.update_stats(self)
character_panel.toggle()
# I drücken = Inventar öffnen/schließen
if Input.is_action_just_pressed("toggle_inventory"):
inventory_panel.toggle()
# Eingabe # Eingabe
var input_dir = Vector2.ZERO var input_dir = Vector2.ZERO
if Input.is_action_pressed("move_forward"): if Input.is_action_pressed("move_forward"):

View file

@ -4,6 +4,8 @@
[ext_resource type="PackedScene" uid="uid://01rrtitc6yh1" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-b.glb" id="2_hqtel"] [ext_resource type="PackedScene" uid="uid://01rrtitc6yh1" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-b.glb" id="2_hqtel"]
[ext_resource type="Script" uid="uid://bwtwon54po4w3" path="res://camera_pivot.gd" id="2_onrkg"] [ext_resource type="Script" uid="uid://bwtwon54po4w3" path="res://camera_pivot.gd" id="2_onrkg"]
[ext_resource type="PackedScene" uid="uid://bej3excyoxrdh" path="res://hud.tscn" id="4_hqtel"] [ext_resource type="PackedScene" uid="uid://bej3excyoxrdh" path="res://hud.tscn" id="4_hqtel"]
[ext_resource type="PackedScene" uid="uid://character_panel" path="res://character_panel.tscn" id="5_char_panel"]
[ext_resource type="PackedScene" uid="uid://inventory_panel" path="res://inventory_panel.tscn" id="6_inv_panel"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4flbx"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4flbx"]
radius = 0.6 radius = 0.6
@ -27,3 +29,7 @@ script = ExtResource("2_onrkg")
transform = Transform3D(2, 0, 0, 0, 1.8126155, 0.84523654, 0, -0.84523654, 1.8126155, 0, 5, 5) transform = Transform3D(2, 0, 0, 0, 1.8126155, 0.84523654, 0, -0.84523654, 1.8126155, 0, 5, 5)
[node name="HUD" parent="." unique_id=1901284390 instance=ExtResource("4_hqtel")] [node name="HUD" parent="." unique_id=1901284390 instance=ExtResource("4_hqtel")]
[node name="CharacterPanel" parent="." instance=ExtResource("5_char_panel")]
[node name="InventoryPanel" parent="." instance=ExtResource("6_inv_panel")]

View file

@ -97,6 +97,16 @@ ui_right_mouse={
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(411, 19),"global_position":Vector2(420, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(411, 19),"global_position":Vector2(420, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
] ]
} }
toggle_character={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
]
}
toggle_inventory={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null)
]
}
[physics] [physics]

View file

@ -2,10 +2,57 @@
# Initialisiert die Spielwelt: weist dem Gegner den Spieler als Ziel zu # Initialisiert die Spielwelt: weist dem Gegner den Spieler als Ziel zu
extends Node3D extends Node3D
const ENEMY_SCENE = preload("res://enemy.tscn")
const CLASS_SELECTION_MENU = preload("res://class_selection_menu.tscn")
const RESPAWN_TIME = 5.0
# Startausrüstung
const STARTER_WEAPON = preload("res://equipment/iron_sword.tres")
const STARTER_CHEST = preload("res://equipment/leather_chest.tres")
@onready var player = $Player
func _ready(): func _ready():
var player = get_node("Player") # Klassenauswahl-Menü anzeigen
var menu = CLASS_SELECTION_MENU.instantiate()
add_child(menu)
menu.class_selected.connect(_on_class_selected)
# Klasse ausgewählt: Spieler initialisieren
func _on_class_selected(character_class: CharacterClass):
player.character_class = character_class
# Startausrüstung geben
player.equip_item(STARTER_WEAPON)
player.equip_item(STARTER_CHEST)
player._calculate_stats()
player.current_hp = player.max_hp
player.hud.update_health(player.current_hp, player.max_hp)
print("Klasse gewählt: ", character_class.class_name_de)
# Jetzt Gegner initialisieren
var enemy = get_node("Enemy") var enemy = get_node("Enemy")
_setup_enemy(enemy)
# Gegner initialisieren und Signal verbinden
func _setup_enemy(enemy):
if enemy and player: if enemy and player:
enemy.target = player enemy.target = player
enemy.enemy_died.connect(_on_enemy_died)
else: else:
print("Fehler: Player oder Enemy nicht gefunden!") print("Fehler: Player oder Enemy nicht gefunden!")
# Gegner gestorben: Nach 5 Sekunden respawnen
func _on_enemy_died(spawn_position: Vector3, _xp_reward: int):
print("Respawn in ", RESPAWN_TIME, " Sekunden...")
await get_tree().create_timer(RESPAWN_TIME).timeout
_spawn_enemy(spawn_position)
# Neuen Gegner an Position spawnen
func _spawn_enemy(position: Vector3):
var new_enemy = ENEMY_SCENE.instantiate()
add_child(new_enemy)
new_enemy.global_position = position
_setup_enemy(new_enemy)
print("Gegner respawned!")