Trink-Animation, Enemy-Level-Skalierung, Droprates angepasst

- Drinking-Animation beim Trank-Benutzen (0.3s Blend, 0.7x Speed)
- Bewegung bricht Trinken ab, Skills blockiert während Trinken
- Enemy mob_level mit Skalierung: HP +30%, Schaden +20%, XP +40%, Gold +50% pro Level
- Droprates gesenkt: Equipment 5%, HP-Trank 15%, Mana-Trank 10%
- Spieler startet mit 3 Heiltränken auf Slot 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andre 2026-03-17 01:56:54 +01:00
parent 9ed18e034c
commit 3766be4017
8 changed files with 109 additions and 10 deletions

View file

@ -232,6 +232,9 @@ Resource-Klasse für verbrauchbare Items (Tränke, Essen, etc.).
- HP-Tränke: Nur wenn HP nicht voll, stellt HP wieder her - HP-Tränke: Nur wenn HP nicht voll, stellt HP wieder her
- Mana-Tränke: Nur wenn Mana nicht voll, stellt Ressource wieder her - Mana-Tränke: Nur wenn Mana nicht voll, stellt Ressource wieder her
- **Stacking:** Gleichnamige Tränke werden automatisch gestackt (max 20) - **Stacking:** Gleichnamige Tränke werden automatisch gestackt (max 20)
- **Trink-Animation:** Beim Benutzen wird die Drinking-Animation abgespielt (0.3s Blend, 0.7x Speed)
- Während des Trinkens: keine Skills, kein weiterer Trank, Bewegung bricht ab
- **Starter-Tränke:** Spieler startet mit 3 kleinen Heiltränken auf Slot 3
--- ---
@ -284,8 +287,12 @@ Ein einzelner Drop-Eintrag.
| item | Resource | Das droppbare Item (Equipment oder Consumable) | | item | Resource | Das droppbare Item (Equipment oder Consumable) |
| drop_chance | float | Wahrscheinlichkeit (0.0 - 1.0) | | drop_chance | float | Wahrscheinlichkeit (0.0 - 1.0) |
### Gold-Skalierung nach Enemy-Level
- `generate_loot(enemy_level)` berechnet Gold: `base_gold * (1.0 + (level-1) * 0.5)`
- Level 1: x1.0, Level 3: x2.0, Level 5: x3.0
### Vorhandene LootTables (loot_tables/) ### Vorhandene LootTables (loot_tables/)
- **goblin_loot.tres** — 2-8 Gold, Eisenschwert (15%), Lederrüstung (10%), Eisenhelm (10%), Heiltrank (40%), Manatrank (25%) - **goblin_loot.tres** — 2-8 Gold, Eisenschwert (5%), Lederrüstung (5%), Eisenhelm (5%), Heiltrank (15%), Manatrank (10%)
- **skeleton_loot.tres** — 5-15 Gold, Stahlschwert (10%), Holzschild (12%), Eisenhelm (15%) - **skeleton_loot.tres** — 5-15 Gold, Stahlschwert (10%), Holzschild (12%), Eisenhelm (15%)
### LootWindow (loot_window.gd) ### LootWindow (loot_window.gd)
@ -375,6 +382,14 @@ Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
| detection_range | 15.0 | | detection_range | 15.0 |
| patrol_radius | 8.0 | | patrol_radius | 8.0 |
| xp_reward | 20 | | xp_reward | 20 |
| mob_level | 1 |
### Level-Skalierung
Stats werden in `_ready()` basierend auf `mob_level` skaliert (Basiswerte = Level 1):
- **HP:** +30% pro Level (`max_hp * (1 + (level-1) * 0.3)`)
- **Schaden:** +20% pro Level (`damage * (1 + (level-1) * 0.2)`)
- **XP:** +40% pro Level (`xp * (1 + (level-1) * 0.4)`)
- **Gold:** +50% pro Level (in `generate_loot`)
### KI-Verhalten (State Machine) ### KI-Verhalten (State Machine)
| State | Beschreibung | | State | Beschreibung |

Binary file not shown.

View file

@ -0,0 +1,44 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bybwuqupuicct"
path="res://.godot/imported/Drinking.fbx-e77657836eed0f61a96552dfc25075c4.scn"
[deps]
source_file="res://assets/Animations/Drinking.fbx"
dest_files=["res://.godot/imported/Drinking.fbx-e77657836eed0f61a96552dfc25075c4.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=true
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
fbx/importer=0
fbx/allow_geometry_helper_nodes=false
fbx/embedded_image_handling=1
fbx/naming_version=2

View file

@ -32,6 +32,7 @@ signal enemy_dropped_loot(loot: Dictionary, world_pos: Vector3)
@export var attack_speed: float = 2.0 # Sekunden zwischen Angriffen @export var attack_speed: float = 2.0 # Sekunden zwischen Angriffen
@export var move_speed: float = 5.5 @export var move_speed: float = 5.5
@export var xp_reward: int = 20 @export var xp_reward: int = 20
@export var mob_level: int = 1
@export var detection_range: float = 15.0 @export var detection_range: float = 15.0
@export var loot_table: LootTable = null @export var loot_table: LootTable = null
@ -81,11 +82,25 @@ var is_turning: bool = false
@onready var health_label: Label3D = $HealthDisplay/Label3D @onready var health_label: Label3D = $HealthDisplay/Label3D
@onready var model: Node3D = $Model @onready var model: Node3D = $Model
# ═══════════════════════════════════════════════════════════════
# LEVEL-SKALIERUNG
# ═══════════════════════════════════════════════════════════════
func _apply_level_scaling():
if mob_level <= 1:
return
var lvl = mob_level - 1
max_hp = int(max_hp * (1.0 + lvl * 0.3)) # +30% HP pro Level
min_damage = int(min_damage * (1.0 + lvl * 0.2)) # +20% Schaden pro Level
max_damage = int(max_damage * (1.0 + lvl * 0.2))
xp_reward = int(xp_reward * (1.0 + lvl * 0.4)) # +40% XP pro Level
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# READY # READY
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
func _ready(): func _ready():
_apply_level_scaling()
current_hp = max_hp current_hp = max_hp
_update_health_display() _update_health_display()
health_label.visible = false health_label.visible = false
@ -400,7 +415,7 @@ func _die():
# Loot generieren # Loot generieren
if loot_table != null: if loot_table != null:
var loot = loot_table.generate_loot() var loot = loot_table.generate_loot(mob_level)
enemy_dropped_loot.emit(loot, global_position) enemy_dropped_loot.emit(loot, global_position)
# XP und Respawn-Signal (nur einmal!) # XP und Respawn-Signal (nur einmal!)

View file

@ -11,9 +11,10 @@ class_name LootTable
@export var possible_drops: Array[LootEntry] = [] @export var possible_drops: Array[LootEntry] = []
# Generiert Loot basierend auf Tabelle # Generiert Loot basierend auf Tabelle
func generate_loot() -> Dictionary: func generate_loot(enemy_level: int = 1) -> Dictionary:
var gold_mult = 1.0 + (enemy_level - 1) * 0.5
var result = { var result = {
"gold": randi_range(min_gold, max_gold), "gold": int(randi_range(min_gold, max_gold) * gold_mult),
"items": [] "items": []
} }

View file

@ -11,25 +11,27 @@
[sub_resource type="Resource" id="entry_1"] [sub_resource type="Resource" id="entry_1"]
script = ExtResource("2") script = ExtResource("2")
item = ExtResource("3") item = ExtResource("3")
drop_chance = 0.15 drop_chance = 0.05
[sub_resource type="Resource" id="entry_2"] [sub_resource type="Resource" id="entry_2"]
script = ExtResource("2") script = ExtResource("2")
item = ExtResource("4") item = ExtResource("4")
drop_chance = 0.05
[sub_resource type="Resource" id="entry_3"] [sub_resource type="Resource" id="entry_3"]
script = ExtResource("2") script = ExtResource("2")
item = ExtResource("5") item = ExtResource("5")
drop_chance = 0.05
[sub_resource type="Resource" id="entry_4"] [sub_resource type="Resource" id="entry_4"]
script = ExtResource("2") script = ExtResource("2")
item = ExtResource("6") item = ExtResource("6")
drop_chance = 0.4 drop_chance = 0.15
[sub_resource type="Resource" id="entry_5"] [sub_resource type="Resource" id="entry_5"]
script = ExtResource("2") script = ExtResource("2")
item = ExtResource("7") item = ExtResource("7")
drop_chance = 0.25 drop_chance = 0.1
[resource] [resource]
script = ExtResource("1") script = ExtResource("1")

View file

@ -56,6 +56,7 @@ const ANIM_WALK_JUMP = "res://assets/Warrior+Animation/Walking Jump.fbx"
const ANIM_RUN_JUMP = "res://assets/Warrior+Animation/Running Jump.fbx" const ANIM_RUN_JUMP = "res://assets/Warrior+Animation/Running Jump.fbx"
const ANIM_ROLL = "res://assets/Warrior+Animation/Quick Roll To Run.fbx" const ANIM_ROLL = "res://assets/Warrior+Animation/Quick Roll To Run.fbx"
const ANIM_TURN_180 = "res://assets/Warrior+Animation/Running Turn 180.fbx" const ANIM_TURN_180 = "res://assets/Warrior+Animation/Running Turn 180.fbx"
const ANIM_DRINKING = "res://assets/Animations/Drinking.fbx"
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# CHARAKTER-STATS # CHARAKTER-STATS
@ -148,6 +149,7 @@ var is_attacking: bool = false
var is_dead: bool = false var is_dead: bool = false
var is_walking: bool = false var is_walking: bool = false
var is_rolling: bool = false var is_rolling: bool = false
var is_drinking: bool = false
var roll_direction: Vector3 = Vector3.ZERO var roll_direction: Vector3 = Vector3.ZERO
var roll_cooldown: float = 0.0 var roll_cooldown: float = 0.0
const ROLL_COOLDOWN: float = 1.5 const ROLL_COOLDOWN: float = 1.5
@ -541,7 +543,7 @@ func _on_slot_clicked(slot_index: int):
_use_consumable_slot(slot_index, slot_content) _use_consumable_slot(slot_index, slot_content)
func execute_skill(skill_id: String): func execute_skill(skill_id: String):
if is_dead or is_casting: if is_dead or is_casting or is_drinking:
return return
var skill = _find_skill(skill_id) var skill = _find_skill(skill_id)
if skill.is_empty(): if skill.is_empty():
@ -672,9 +674,12 @@ func use_consumable(item: Consumable) -> bool:
if consumable_cooldowns.get(item.item_name, 0.0) > 0: if consumable_cooldowns.get(item.item_name, 0.0) > 0:
print(item.item_name + " im Cooldown!") print(item.item_name + " im Cooldown!")
return false return false
if is_drinking:
return false
var used = item.use(self) var used = item.use(self)
if used: if used:
consumable_cooldowns[item.item_name] = item.cooldown consumable_cooldowns[item.item_name] = item.cooldown
_play_drinking_anim()
return used return used
func _use_consumable_slot(slot_index: int, item: Consumable): func _use_consumable_slot(slot_index: int, item: Consumable):
@ -755,6 +760,7 @@ func _setup_animations():
_load_anim_from_fbx(ANIM_RUN_JUMP, "run_jump", false) _load_anim_from_fbx(ANIM_RUN_JUMP, "run_jump", false)
_load_anim_from_fbx(ANIM_ROLL, "roll", false) _load_anim_from_fbx(ANIM_ROLL, "roll", false)
_load_anim_from_fbx(ANIM_TURN_180, "turn_180", false) _load_anim_from_fbx(ANIM_TURN_180, "turn_180", false)
_load_anim_from_fbx(ANIM_DRINKING, "drinking", false)
_play_anim("idle") _play_anim("idle")
@ -876,6 +882,15 @@ func _play_anim_once(name: String):
await anim_player.animation_finished await anim_player.animation_finished
is_attacking = false is_attacking = false
func _play_drinking_anim():
if anim_player == null or not anim_player.has_animation("drinking"):
return
is_drinking = true
anim_player.speed_scale = 1.0
anim_player.play("drinking", 0.3, 0.7, false)
await anim_player.animation_finished
is_drinking = false
func _do_roll(): func _do_roll():
# Bewegungsrichtung beim Roll-Start erfassen # Bewegungsrichtung beim Roll-Start erfassen
@ -900,7 +915,7 @@ func _do_roll():
is_rolling = false is_rolling = false
func _update_movement_animation(is_moving: bool, input_dir: Vector2): func _update_movement_animation(is_moving: bool, input_dir: Vector2):
if is_attacking or is_dead or is_rolling: if is_attacking or is_dead or is_rolling or is_drinking:
return return
var has_target = target != null and is_instance_valid(target) var has_target = target != null and is_instance_valid(target)
if not is_on_floor(): if not is_on_floor():
@ -1027,9 +1042,11 @@ func _physics_process(delta):
# get_vector: x = links(-)/rechts(+), y = vor(-)/zurück(+) # get_vector: x = links(-)/rechts(+), y = vor(-)/zurück(+)
var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back") var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# Cast unterbrechen bei Bewegung # Cast/Trinken unterbrechen bei Bewegung
if is_casting and input_dir.length() > 0.1: if is_casting and input_dir.length() > 0.1:
_cancel_cast() _cancel_cast()
if is_drinking and input_dir.length() > 0.1:
is_drinking = false
var is_moving = input_dir.length() > 0.1 var is_moving = input_dir.length() > 0.1

View file

@ -20,6 +20,7 @@ const RESPAWN_TIME = 5.0
const STARTER_SWORD = 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_STAFF = preload("res://equipment/wooden_staff.tres")
const STARTER_CHEST = preload("res://equipment/leather_chest.tres") const STARTER_CHEST = preload("res://equipment/leather_chest.tres")
const STARTER_POTION = preload("res://consumables/small_hp_potion.tres")
# Loot Tables # Loot Tables
const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres") const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres")
@ -105,6 +106,10 @@ func _on_class_selected(character_class: CharacterClass):
player._calculate_stats() player._calculate_stats()
player.current_hp = player.max_hp player.current_hp = player.max_hp
var potion = STARTER_POTION.duplicate()
potion.stack_size = 3
player.inventory.add_item(potion)
player.assign_to_action_bar(2, potion)
player.current_resource = player.max_resource player.current_resource = player.max_resource
player.hud.update_health(player.current_hp, player.max_hp) player.hud.update_health(player.current_hp, player.max_hp)
player.hud.update_resource(player.current_resource, player.max_resource, player.get_resource_name()) player.hud.update_resource(player.current_resource, player.max_resource, player.get_resource_name())