From b01059de50a4223ae9703b55e15829f7032450e7 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 15 Mar 2026 20:14:22 +0100 Subject: [PATCH] =?UTF-8?q?Loot-System,=20Gegner-Drops=20und=20Gold-Anzeig?= =?UTF-8?q?e=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enemy droppt Loot bei Tod (Gold + Items basierend auf LootTable) - LootWindow zeigt Beute an mit "Alles aufheben" Button - Gold-Anzeige im HUD unter XP-Leiste - Beispiel LootTables: Goblin (2-8 Gold) und Skeleton (5-15 Gold) - Loot-System in World verdrahtet Co-Authored-By: Claude Opus 4.6 --- enemy.gd | 20 ++++++ hud.gd | 15 +++++ loot_tables/goblin_loot.tres | 28 ++++++++ loot_tables/skeleton_loot.tres | 28 ++++++++ loot_window.gd | 118 +++++++++++++++++++++++++++++++++ loot_window.tscn | 61 +++++++++++++++++ player.gd | 11 +++ player.tscn | 3 + world.gd | 11 +++ 9 files changed, 295 insertions(+) create mode 100644 loot_tables/goblin_loot.tres create mode 100644 loot_tables/skeleton_loot.tres create mode 100644 loot_window.gd create mode 100644 loot_window.tscn diff --git a/enemy.gd b/enemy.gd index 3091c2d..2fc6e7c 100644 --- a/enemy.gd +++ b/enemy.gd @@ -3,6 +3,7 @@ extends CharacterBody3D signal enemy_died(spawn_position: Vector3, xp_reward: int) +signal enemy_dropped_loot(loot: Dictionary, world_position: Vector3) const SPEED = 3.0 const PATROL_SPEED = 1.5 @@ -36,6 +37,9 @@ var attack_damage: int = 5 # XP-Belohnung (skaliert mit Level) var xp_reward: int = 25 +# Loot-System +@export var loot_table: LootTable + var target = null # Spieler-Referenz (wird von World gesetzt) var can_attack = true var spawn_position: Vector3 # Ursprüngliche Spawn-Position @@ -119,9 +123,25 @@ func die(): # XP an Spieler geben if target and target.has_method("gain_xp"): target.gain_xp(xp_reward) + # Loot generieren und droppen + _drop_loot() enemy_died.emit(spawn_position, xp_reward) queue_free() +# Loot generieren basierend auf LootTable +func _drop_loot(): + if loot_table == null: + # Standard-Gold-Drop wenn keine LootTable zugewiesen + var gold = randi_range(1, 3) * level + var loot = {"gold": gold, "items": []} + enemy_dropped_loot.emit(loot, global_position) + return + + var loot = loot_table.generate_loot() + # Gold mit Level skalieren + loot["gold"] = loot["gold"] * level + enemy_dropped_loot.emit(loot, global_position) + func _physics_process(delta): if not is_on_floor(): velocity.y -= GRAVITY * delta diff --git a/hud.gd b/hud.gd index 1676c3e..440e2bb 100644 --- a/hud.gd +++ b/hud.gd @@ -10,6 +10,7 @@ signal slot_clicked(slot_index: int) # Level/XP UI (wird dynamisch erstellt) var level_label: Label var xp_bar: ProgressBar +var gold_label: Label @onready var action_slots = [ $Control/ActionBar/A1, $Control/ActionBar/A2, @@ -130,6 +131,20 @@ func _create_level_ui(): control.add_child(xp_bar) + # Gold Label + gold_label = Label.new() + gold_label.name = "GoldLabel" + gold_label.position = Vector2(20, 78) + gold_label.add_theme_font_size_override("font_size", 14) + gold_label.add_theme_color_override("font_color", Color(1, 0.85, 0, 1)) + gold_label.text = "0 Gold" + control.add_child(gold_label) + +# Gold aktualisieren +func update_gold(amount: int): + if gold_label: + gold_label.text = str(amount) + " Gold" + # HP-Leiste und Text aktualisieren func update_health(current_hp, max_hp): health_bar.max_value = max_hp diff --git a/loot_tables/goblin_loot.tres b/loot_tables/goblin_loot.tres new file mode 100644 index 0000000..6f33f9c --- /dev/null +++ b/loot_tables/goblin_loot.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" script_class="LootTable" format=3] + +[ext_resource type="Script" path="res://loot_table.gd" id="1"] +[ext_resource type="Script" path="res://loot_entry.gd" id="2"] +[ext_resource type="Resource" path="res://equipment/iron_sword.tres" id="3"] +[ext_resource type="Resource" path="res://equipment/leather_chest.tres" id="4"] +[ext_resource type="Resource" path="res://equipment/iron_helm.tres" id="5"] + +[sub_resource type="Resource" id="entry_1"] +script = ExtResource("2") +item = ExtResource("3") +drop_chance = 0.15 + +[sub_resource type="Resource" id="entry_2"] +script = ExtResource("2") +item = ExtResource("4") +drop_chance = 0.1 + +[sub_resource type="Resource" id="entry_3"] +script = ExtResource("2") +item = ExtResource("5") +drop_chance = 0.1 + +[resource] +script = ExtResource("1") +min_gold = 2 +max_gold = 8 +possible_drops = [SubResource("entry_1"), SubResource("entry_2"), SubResource("entry_3")] diff --git a/loot_tables/skeleton_loot.tres b/loot_tables/skeleton_loot.tres new file mode 100644 index 0000000..46812a3 --- /dev/null +++ b/loot_tables/skeleton_loot.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" script_class="LootTable" format=3] + +[ext_resource type="Script" path="res://loot_table.gd" id="1"] +[ext_resource type="Script" path="res://loot_entry.gd" id="2"] +[ext_resource type="Resource" path="res://equipment/steel_sword.tres" id="3"] +[ext_resource type="Resource" path="res://equipment/wooden_shield.tres" id="4"] +[ext_resource type="Resource" path="res://equipment/iron_helm.tres" id="5"] + +[sub_resource type="Resource" id="entry_1"] +script = ExtResource("2") +item = ExtResource("3") +drop_chance = 0.1 + +[sub_resource type="Resource" id="entry_2"] +script = ExtResource("2") +item = ExtResource("4") +drop_chance = 0.12 + +[sub_resource type="Resource" id="entry_3"] +script = ExtResource("2") +item = ExtResource("5") +drop_chance = 0.15 + +[resource] +script = ExtResource("1") +min_gold = 5 +max_gold = 15 +possible_drops = [SubResource("entry_1"), SubResource("entry_2"), SubResource("entry_3")] diff --git a/loot_window.gd b/loot_window.gd new file mode 100644 index 0000000..494bb49 --- /dev/null +++ b/loot_window.gd @@ -0,0 +1,118 @@ +# LootWindow.gd +# Zeigt Loot eines besiegten Gegners an und lässt Spieler Items aufheben +extends CanvasLayer + +var player = null +var current_loot: Dictionary = {} # {"gold": int, "items": [Equipment]} +var loot_world_position: Vector3 = Vector3.ZERO +var panel_visible = false + +const LOOT_PICKUP_RANGE = 5.0 + +@onready var panel = $Panel +@onready var gold_label = $Panel/VBoxContainer/GoldLabel +@onready var item_list = $Panel/VBoxContainer/ScrollContainer/ItemList +@onready var loot_all_button = $Panel/VBoxContainer/LootAllButton + +func _ready(): + panel.visible = false + loot_all_button.pressed.connect(_on_loot_all) + +func setup(p): + player = p + +# Loot anzeigen +func show_loot(loot: Dictionary, world_pos: Vector3): + current_loot = loot + loot_world_position = world_pos + _refresh_display() + panel_visible = true + panel.visible = true + +func hide_loot(): + panel_visible = false + panel.visible = false + current_loot = {} + +func _refresh_display(): + # Gold anzeigen + if current_loot.get("gold", 0) > 0: + gold_label.text = str(current_loot["gold"]) + " Gold" + gold_label.visible = true + else: + gold_label.visible = false + + # Items anzeigen + for child in item_list.get_children(): + child.queue_free() + + var items = current_loot.get("items", []) + for i in range(items.size()): + var item = items[i] + var button = Button.new() + button.text = item.item_name + " (" + Equipment.get_slot_name(item.slot) + ")" + button.modulate = Equipment.get_rarity_color(item.rarity) + button.pressed.connect(_on_loot_item.bind(i)) + button.tooltip_text = _get_item_tooltip(item) + item_list.add_child(button) + + # Wenn kein Loot mehr da, Fenster schließen + if current_loot.get("gold", 0) <= 0 and items.size() == 0: + hide_loot() + +# Einzelnes Item aufheben +func _on_loot_item(index: int): + if player == null: + return + + var items = current_loot.get("items", []) + if index >= items.size(): + return + + var item = items[index] + if player.inventory.add_item(item): + items.remove_at(index) + _refresh_display() + +# Alles aufheben +func _on_loot_all(): + if player == null: + return + + # Gold aufheben + var gold = current_loot.get("gold", 0) + if gold > 0: + player.inventory.add_gold(gold) + current_loot["gold"] = 0 + + # Items aufheben + var items = current_loot.get("items", []) + var remaining = [] + for item in items: + if not player.inventory.add_item(item): + remaining.append(item) # Inventar voll + current_loot["items"] = remaining + + _refresh_display() + +func _get_item_tooltip(item: Equipment) -> String: + var tooltip = item.item_name + "\n" + tooltip += Equipment.get_slot_name(item.slot) + "\n" + + if item.slot == Equipment.Slot.WEAPON: + tooltip += "Schaden: " + str(item.min_damage) + "-" + str(item.max_damage) + "\n" + tooltip += "Tempo: " + str(item.attack_speed) + "s\n" + if item.armor > 0: + tooltip += "Rüstung: +" + str(item.armor) + "\n" + if item.strength > 0: + tooltip += "Stärke: +" + str(item.strength) + "\n" + if item.agility > 0: + tooltip += "Beweglichkeit: +" + str(item.agility) + "\n" + if item.intelligence > 0: + tooltip += "Intelligenz: +" + str(item.intelligence) + "\n" + if item.stamina > 0: + tooltip += "Ausdauer: +" + str(item.stamina) + "\n" + if item.haste > 0: + tooltip += "Tempo: +" + str(int(item.haste * 100)) + "%\n" + + return tooltip diff --git a/loot_window.tscn b/loot_window.tscn new file mode 100644 index 0000000..569efd0 --- /dev/null +++ b/loot_window.tscn @@ -0,0 +1,61 @@ +[gd_scene format=3 uid="uid://loot_window"] + +[ext_resource type="Script" path="res://loot_window.gd" id="1_loot"] + +[node name="LootWindow" type="CanvasLayer"] +script = ExtResource("1_loot") + +[node name="Panel" type="Panel" parent="."] +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -130.0 +offset_top = -350.0 +offset_right = 130.0 +offset_bottom = -150.0 +grow_horizontal = 2 +grow_vertical = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 6 + +[node name="Title" type="Label" parent="Panel/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 16 +text = "Beute" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"] +layout_mode = 2 + +[node name="GoldLabel" type="Label" parent="Panel/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +theme_override_colors/font_color = Color(1, 0.85, 0, 1) +text = "0 Gold" + +[node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ItemList" type="VBoxContainer" parent="Panel/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 4 + +[node name="LootAllButton" type="Button" parent="Panel/VBoxContainer"] +layout_mode = 2 +text = "Alles aufheben" diff --git a/player.gd b/player.gd index 9af95da..e87a7ca 100644 --- a/player.gd +++ b/player.gd @@ -61,6 +61,7 @@ const HEAVY_STRIKE_RANGE = 4.0 @onready var hud = $HUD @onready var character_panel = $CharacterPanel @onready var inventory_panel = $InventoryPanel +@onready var loot_window = $LootWindow func _ready(): # Stats aus Klasse berechnen @@ -80,6 +81,12 @@ func _ready(): # Inventar Panel initialisieren inventory_panel.setup(self) + # Loot Window initialisieren + loot_window.setup(self) + + # Gold im HUD aktualisieren wenn sich Gold ändert + inventory.gold_changed.connect(func(amount): hud.update_gold(amount)) + # Stats basierend auf Klasse und Level berechnen func _calculate_stats(): if character_class == null: @@ -251,6 +258,10 @@ func heal(amount): current_hp = clamp(current_hp + amount, 0, max_hp) hud.update_health(current_hp, max_hp) +# Loot empfangen und Fenster anzeigen +func receive_loot(loot: Dictionary, world_pos: Vector3): + loot_window.show_loot(loot, world_pos) + func die(): print("Spieler gestorben!") diff --git a/player.tscn b/player.tscn index 4769dc9..df39355 100644 --- a/player.tscn +++ b/player.tscn @@ -6,6 +6,7 @@ [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"] +[ext_resource type="PackedScene" uid="uid://loot_window" path="res://loot_window.tscn" id="7_loot_win"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4flbx"] radius = 0.6 @@ -33,3 +34,5 @@ transform = Transform3D(2, 0, 0, 0, 1.8126155, 0.84523654, 0, -0.84523654, 1.812 [node name="CharacterPanel" parent="." instance=ExtResource("5_char_panel")] [node name="InventoryPanel" parent="." instance=ExtResource("6_inv_panel")] + +[node name="LootWindow" parent="." instance=ExtResource("7_loot_win")] diff --git a/world.gd b/world.gd index 053a2fb..fd14393 100644 --- a/world.gd +++ b/world.gd @@ -10,6 +10,9 @@ const RESPAWN_TIME = 5.0 const STARTER_WEAPON = preload("res://equipment/iron_sword.tres") const STARTER_CHEST = preload("res://equipment/leather_chest.tres") +# Loot Tables +const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres") + @onready var player = $Player func _ready(): @@ -39,10 +42,18 @@ func _on_class_selected(character_class: CharacterClass): func _setup_enemy(enemy): if enemy and player: enemy.target = player + if enemy.loot_table == null: + enemy.loot_table = GOBLIN_LOOT enemy.enemy_died.connect(_on_enemy_died) + enemy.enemy_dropped_loot.connect(_on_enemy_dropped_loot) else: print("Fehler: Player oder Enemy nicht gefunden!") +# Loot-Drop an Spieler weiterleiten +func _on_enemy_dropped_loot(loot: Dictionary, world_pos: Vector3): + if player: + player.receive_loot(loot, world_pos) + # Gegner gestorben: Nach 5 Sekunden respawnen func _on_enemy_died(spawn_position: Vector3, _xp_reward: int): print("Respawn in ", RESPAWN_TIME, " Sekunden...")