Enemy-System komplett überarbeitet, Kamera-Steuerung verbessert

- Enemy: Neues castle_guard_01 Modell mit Animationen (idle, walk, run, autoattack, death, turn)
- Enemy: Patrol-KI mit Turn-Animationen beim Richtungswechsel, 5s idle nach Spawn
- Enemy: Aggro durch Detection Range (15m) und Schadens-Aggro, Patrol→Chase Übergang
- Enemy: Respawn nach 5s am Spawnpunkt, XP-Vergabe beim Tod
- Kamera: LMB frei drehen (umschauen) auch mit markiertem Ziel
- Kamera: RMB Lock-On temporär aufheben zum Weglaufen
- Kamera: LMB-Klick auf freie Fläche visiert Ziel ab
- Kamera: Drag vs Klick Unterscheidung (< 5px Bewegung = Klick)
- Autoattack greift automatisch wieder an wenn Ziel zurück in Range
- Player zur Gruppe "player" hinzugefügt für Enemy-Detection
- Dokumentation vollständig aktualisiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andre 2026-03-17 00:56:14 +01:00
parent 752086bd1b
commit e4efb239f2
24 changed files with 473 additions and 152 deletions

View file

@ -20,15 +20,17 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge
|---|---| |---|---|
| W / A / S / D | Bewegen | | W / A / S / D | Bewegen |
| Mausrad | Kamera zoomen (min 5, max 20) | | Mausrad | Kamera zoomen (min 5, max 20) |
| RMB gehalten | Kamera drehen, Spieler schaut mit | | LMB gehalten + Mausbewegung | Kamera frei drehen (umschauen), Charakter bleibt in Position |
| Linksklick auf Gegner | Ziel markieren | | LMB Klick auf Gegner | Ziel markieren |
| Rechtsklick auf Gegner | Ziel markieren + Autoattack starten | | LMB Klick auf freie Fläche | Ziel abvisieren |
| RMB gehalten + Mausbewegung | Spieler + Kamera drehen (auch mit Target → weglaufen) |
| RMB Klick auf Gegner | Ziel markieren + Autoattack starten |
| 1 9 | Aktionsleiste Slots (Skills + Consumables, frei belegbar) | | 1 9 | Aktionsleiste Slots (Skills + Consumables, frei belegbar) |
| C | Charakter-Panel (Stats + Equipment) | | C | Charakter-Panel (Stats + Equipment) |
| I | Inventar öffnen/schließen | | I | Inventar öffnen/schließen |
| P | Fähigkeiten-Panel (alle Skills, Drag auf Aktionsleiste) | | P | Fähigkeiten-Panel (alle Skills, Drag auf Aktionsleiste) |
| Leertaste | Springen | | Leertaste | Springen |
| T | (Test) 10 Schaden am Spieler | | NumLock | Walk/Run umschalten |
--- ---
@ -40,8 +42,8 @@ Hauptszene der Spielwelt. Zeigt bei Start das Hauptmenü (Einstellungen), dann K
World (Node3D) World (Node3D)
├── Player (player.tscn) ├── Player (player.tscn)
├── Enemy (enemy.tscn) ├── Enemy (enemy.tscn)
├── StaticBody3D (Boden) ├── Boden (StaticBody3D)
│ ├── MeshInstance3D │ ├── MeshInstance3D (Schachbrett-Shader)
│ └── CollisionShape3D │ └── CollisionShape3D
├── DirectionalLight3D ├── DirectionalLight3D
└── NavigationRegion3D └── NavigationRegion3D
@ -51,31 +53,43 @@ World (Node3D)
Der Spielercharakter mit allen UI-Panels. Der Spielercharakter mit allen UI-Panels.
``` ```
Player (CharacterBody3D) Player (CharacterBody3D)
├── PlayerModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer) ├── Model (Node3D)
│ └── castle_guard_01 (Mixamo FBX mit Skeleton3D + AnimationPlayer)
├── CollisionShape3D ├── CollisionShape3D
├── CameraPivot (Node3D) ├── CameraPivot (Node3D + camera_pivot.gd)
│ └── Camera3D │ └── Camera3D
├── HUD (hud.tscn) └── HUD (hud.tscn)
├── CharacterPanel (character_panel.tscn)
├── InventoryPanel (inventory_panel.tscn)
├── LootWindow (loot_window.tscn)
└── SkillPanel (skill_panel.tscn)
``` ```
UI-Panels (CharacterPanel, InventoryPanel, LootWindow, SkillPanel) werden zur Laufzeit erstellt.
### enemy.tscn ### enemy.tscn
Ein Gegner mit Level-basiertem Stats-System. Ein Gegner mit Patrol-KI und Kampf-Animationen.
``` ```
Enemy (CharacterBody3D) Enemy (CharacterBody3D)
├── EnemyModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer) ├── Model (castle_guard_01.fbx — Mixamo Charakter mit Skeleton3D + AnimationPlayer)
├── CollisionShape3D ├── CollisionShape3D
├── Area3D
│ └── CollisionShape3D
├── NavigationAgent3D ├── NavigationAgent3D
└── HealthLabel (Label3D) └── HealthDisplay (Node3D)
└── Label3D
``` ```
--- ---
## Kamera-System (camera_pivot.gd)
### Modi
| Situation | Verhalten |
|---|---|
| Kein Ziel, LMB gehalten | Kamera orbitet frei, Spieler dreht sich nicht |
| Kein Ziel, RMB gehalten | Spieler + Kamera drehen sich gemeinsam |
| Ziel markiert, nichts gedrückt | Soft Lock-On: Spieler dreht sich smooth zum Ziel |
| Ziel markiert, LMB gehalten | Lock-On pausiert, Kamera frei drehbar (umschauen) |
| Ziel markiert, RMB gehalten | Lock-On pausiert, Spieler+Kamera drehen (weglaufen) |
Lock-On springt automatisch zurück sobald LMB/RMB losgelassen wird und Ziel noch markiert ist.
---
## Klassen-System ## Klassen-System
### CharacterClass (character_class.gd) ### CharacterClass (character_class.gd)
@ -133,9 +147,10 @@ Die Ressourcen-Leiste wird nur angezeigt wenn die Klasse eine Ressource hat.
## Level-System ## Level-System
- **XP-Kurve:** Level N benötigt `100 * N` XP (Level 2: 100, Level 3: 200, ...) - **XP-Kurve:** Level N benötigt `100 * 1.5^(N-2)` XP
- **Stats pro Level:** Basierend auf Klassen-Zuwachsraten - **Stats pro Level:** Basierend auf Klassen-Zuwachsraten
- **HP bei Level-Up:** Werden vollständig aufgefüllt (HP + Klassen-Ressource) - **HP bei Level-Up:** Werden vollständig aufgefüllt (HP + Klassen-Ressource)
- **XP-Vergabe:** Gegner geben XP basierend auf `xp_reward` Export-Variable
- **Character Panel:** Aktualisiert sich automatisch bei Level-Up - **Character Panel:** Aktualisiert sich automatisch bei Level-Up
--- ---
@ -230,6 +245,8 @@ Resource-Klasse für das Spieler-Inventar.
| add_gold(amount) | Gold hinzufügen | | add_gold(amount) | Gold hinzufügen |
| spend_gold(amount) | Gold ausgeben (false wenn nicht genug) | | spend_gold(amount) | Gold ausgeben (false wenn nicht genug) |
| is_full() | Prüfen ob Inventar voll | | is_full() | Prüfen ob Inventar voll |
| swap_items(a, b) | Zwei Items tauschen |
| move_item(from, to) | Item verschieben |
### Inventory Panel (inventory_panel.gd, I-Taste) ### Inventory Panel (inventory_panel.gd, I-Taste)
- 5x4 Grid mit Item-Slots - 5x4 Grid mit Item-Slots
@ -237,6 +254,7 @@ Resource-Klasse für das Spieler-Inventar.
- Items in Seltenheitsfarbe - Items in Seltenheitsfarbe
- Tooltips mit vollständigen Item-Stats - Tooltips mit vollständigen Item-Stats
- **Rechtsklick** auf Item: Direkt anlegen (tauscht mit aktuellem Equipment) - **Rechtsklick** auf Item: Direkt anlegen (tauscht mit aktuellem Equipment)
- **Drag & Drop:** Items innerhalb des Inventars verschieben, auf Aktionsleiste ziehen
--- ---
@ -288,6 +306,12 @@ Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
4. **Level-Differenz:** `±10% pro Level, max ±50%` 4. **Level-Differenz:** `±10% pro Level, max ±50%`
5. **Mindestschaden:** Immer mindestens 1 5. **Mindestschaden:** Immer mindestens 1
### Autoattack
- Startet per RMB-Klick auf Gegner oder manuell per Taste 1
- Greift automatisch an sobald GCD abgelaufen und Ziel in Reichweite
- Stoppt wenn Ziel stirbt oder abvisiert wird
- Startet neu wenn man wieder in Range kommt (solange aktiv)
### DPS-Berechnung ### DPS-Berechnung
`DPS = (Durchschnittsschaden + Stat-Bonus) / GCD` `DPS = (Durchschnittsschaden + Stat-Bonus) / GCD`
@ -297,7 +321,6 @@ Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
- **Schaden:** Waffenschaden oder klassenabhängig unbewaffnet + Main-Stat Bonus - **Schaden:** Waffenschaden oder klassenabhängig unbewaffnet + Main-Stat Bonus
- **Cooldown:** GCD (Waffen-Attackspeed / Haste) - **Cooldown:** GCD (Waffen-Attackspeed / Haste)
- **Reichweite:** Waffen-Reichweite oder 3.0 (unbewaffnet) - **Reichweite:** Waffen-Reichweite oder 3.0 (unbewaffnet)
- Automatisch per Rechtsklick oder manuell per Taste 1
- **Animation:** autoattack - **Animation:** autoattack
#### Zauberstab (Taste 1, Magier) #### Zauberstab (Taste 1, Magier)
@ -326,7 +349,6 @@ Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
- Aktionsleiste mit 9 Slots (Taste 1-9), frei belegbar mit Skills und Consumables - Aktionsleiste mit 9 Slots (Taste 1-9), frei belegbar mit Skills und Consumables
- Drag & Drop: Skills/Tränke zwischen Slots verschieben, aus Leiste rausziehen zum Entfernen - Drag & Drop: Skills/Tränke zwischen Slots verschieben, aus Leiste rausziehen zum Entfernen
- Fähigkeiten-Panel (P-Taste): Listet alle verfügbaren Skills, per Drag auf Aktionsleiste ziehen - Fähigkeiten-Panel (P-Taste): Listet alle verfügbaren Skills, per Drag auf Aktionsleiste ziehen
- Icons werden beim Spielstart geladen
- Cooldown-Anzeige: Dunkle Überlagerung + verbleibende Zeit - Cooldown-Anzeige: Dunkle Überlagerung + verbleibende Zeit
- Gelber Highlight-Rand beim Drag über Slots - Gelber Highlight-Rand beim Drag über Slots
@ -334,33 +356,89 @@ Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
## Gegner-System (enemy.gd) ## Gegner-System (enemy.gd)
### Stats ### Stats (Export-Variablen)
Level-basiert mit automatischer Skalierung: | Stat | Standard |
| Stat | Formel |
|---|---| |---|---|
| Stärke | base_strength + (level-1) * 2 | | max_hp | 50 |
| Ausdauer | base_stamina + (level-1) * 3 | | min_damage / max_damage | 3-7 |
| Rüstung | base_armor + (level-1) * 2 | | attack_range | 2.0 |
| HP | Ausdauer * 10 | | attack_speed | 2.0s |
| Schaden | Stärke * 0.5 + 2 | | move_speed | 5.5 (Rennen bei Aggro) |
| XP-Belohnung | 25 * Level | | patrol_speed | 1.5 (Laufen bei Patrol) |
| detection_range | 15.0 |
| patrol_radius | 8.0 |
| xp_reward | 20 |
### KI-Verhalten (State Machine) ### KI-Verhalten (State Machine)
| State | Beschreibung | | State | Beschreibung |
|---|---| |---|---|
| PATROL | Zufällig im Radius um Spawn-Position herumlaufen | | IDLE | Wartet (wird initial für 5s nach Spawn verwendet) |
| CHASE | Spieler verfolgen (Aggro-Range: 8.0) | | PATROL | Läuft zwischen zufälligen Punkten im Spawn-Radius, walk-Animation, Turn-Animationen beim Richtungswechsel |
| ATTACK | Angreifen wenn in Reichweite (1.5) | | CHASING | Rennt zum Spieler (run-Animation), aktiviert bei detection_range oder Schaden |
| ATTACKING | Steht, dreht sich zum Spieler, greift in attack_speed-Intervallen an |
| DEAD | Spielt death-Animation, wird nach 2s entfernt |
### Patrol-System
- Enemy patrouilliert um seinen Spawnpunkt im konfigurierbaren Radius
- Wartet 2-5 Sekunden zwischen Patrol-Punkten (idle-Animation)
- Spielt Turn-Animation (left_turn_90 / right_turn_90) vor dem Loslaufen
- Läuft mit langsamer patrol_speed und walk-Animation
- Bei Aggro sofort Wechsel zu run-Animation und move_speed
### Aggro
- **Detection Range:** Spieler wird automatisch erkannt wenn in 15m Reichweite
- **Schaden-Aggro:** Bei Schaden sofort Wechsel zu CHASING, auch aus PATROL
- **Spieler-Suche:** Per `get_nodes_in_group("player")`
### Respawn ### Respawn
- Gegner spawnen nach 5 Sekunden am Ursprungsort neu - Gegner spawnen nach 5 Sekunden am Ursprungsort neu
- Verwaltet durch world.gd - Verwaltet durch world.gd (`_on_enemy_died` Signal)
### Animationen
Werden zur Laufzeit aus FBX-Dateien geladen (gleiche Methode wie Player):
- idle, walk, run, autoattack, death, turn_left, turn_right
- Root Motion wird automatisch entfernt (`_strip_root_motion`)
### Loot-Drops ### Loot-Drops
- Jeder Gegner hat eine optionale `loot_table` (LootTable Resource) - Jeder Gegner hat eine optionale `loot_table` (LootTable Resource)
- Gold skaliert mit Gegner-Level - Gold skaliert mit Gegner-Level
- Ohne LootTable: Standard-Gold-Drop (1-3 * Level) - Ohne LootTable: Standard-Gold-Drop
---
## Animations-System
### Mixamo-Integration
Charaktermodelle stammen von Mixamo (castle_guard_01.fbx) und werden mit separaten Animations-FBX-Dateien kombiniert.
**Lade-Prozess (player.gd / enemy.gd):**
1. `_setup_animations()` findet den AnimationPlayer im Modell
2. Jede Animations-FBX wird als PackedScene geladen (`_load_anim_from_fbx`)
3. Root Motion wird entfernt (`_strip_root_motion` — XZ-Position nullen, Y behalten)
4. Loop-Modus wird gesetzt (Bewegungsanimationen loopen, Angriff/Tod nicht)
5. Animation wird der AnimationLibrary hinzugefügt
### Verfügbare Animationen (assets/Warrior+Animation/)
| Animation | Datei | Loop | Verwendung |
|---|---|---|---|
| idle | idle.fbx | Ja | Stillstehen |
| walk | walking.fbx | Ja | Vorwärts laufen |
| run | running.fbx | Ja | Rennen |
| walk_back | Walking Backwards.fbx | Ja | Rückwärts laufen |
| strafe_left | left strafe walking.fbx | Ja | Links seitwärts |
| strafe_right | right strafe walking.fbx | Ja | Rechts seitwärts |
| run_strafe_left | Running Left Strafe.fbx | Ja | Rennen links |
| run_strafe_right | Running Right Strafe run.fbx | Ja | Rennen rechts |
| jump | jump.fbx | Nein | Springen |
| walk_jump | Walking Jump.fbx | Nein | Sprung beim Laufen |
| run_jump | Running Jump.fbx | Nein | Sprung beim Rennen |
| autoattack | Autoattack.fbx | Nein | Autoattack / Zauberstab |
| heavy_strike | Heavy Strike.fbx | Nein | Heavy Strike Skill |
| death | Dying Backwards.fbx | Nein | Tod |
| roll | Quick Roll To Run.fbx | Nein | Ausweichrolle |
| turn_180 | Running Turn 180.fbx | Nein | 180°-Drehung |
| turn_left | Left Turn 90.fbx | Nein | 90° links (Enemy Patrol) |
| turn_right | Right Turn 90.fbx | Nein | 90° rechts (Enemy Patrol) |
--- ---
@ -374,42 +452,12 @@ Level-basiert mit automatischer Skalierung:
| XPBar | Blaue XP-Leiste | | XPBar | Blaue XP-Leiste |
| GoldLabel | Gold-Anzeige in Goldfarbe | | GoldLabel | Gold-Anzeige in Goldfarbe |
| ActionBar | 9 Slots mit Icons, Cooldowns, Klick-Support, Stack-Anzeige für Consumables | | ActionBar | 9 Slots mit Icons, Cooldowns, Klick-Support, Stack-Anzeige für Consumables |
| Castbar | Zauberbalken über der Aktionsleiste (eigener CanvasLayer, layer 10) |
---
## Animations-System
### Mixamo-Integration
Charaktermodelle stammen von Mixamo (warrior.fbx) und werden mit separaten Animations-FBX-Dateien kombiniert.
**Lade-Prozess (player.gd / enemy.gd):**
1. `_setup_animations()` findet den AnimationPlayer im Modell
2. Jede Animations-FBX wird als PackedScene geladen
3. Die Animation wird extrahiert und der AnimationLibrary hinzugefügt
4. `_update_animation()` wählt basierend auf Bewegungszustand die passende Animation
### Verfügbare Animationen (assets/animations/)
| Animation | Datei | Verwendung |
|---|---|---|
| walk | Walking.fbx | Vorwärts laufen |
| walk_back | Walking Backwards.fbx | Rückwärts laufen |
| strafe_left | Left Strafe Walking.fbx | Links seitwärts |
| strafe_right | Right Strafe Walking.fbx | Rechts seitwärts |
| jump | Jumping.fbx | Springen |
| autoattack | Autoattack.fbx | Autoattack / Zauberstab |
| heavy_strike | Heavy Strike.fbx | Heavy Strike Skill |
| die | Dying Backwards.fbx | Tod (Spieler + Gegner) |
### Offene Punkte
- [ ] Enemy Walk-Animation: Track-Pfade matchen noch nicht korrekt (Debug in Arbeit)
- [ ] Idle-Animation fehlt (kein FBX vorhanden)
- [ ] Cast-Animation fehlt (für Frostblitz)
--- ---
## Geplante Features ## Geplante Features
- [ ] Wut-Ressource für Krieger - [ ] Wut-Ressource für Krieger
- [ ] Ressourcen-System für Gegner (nicht alle haben Mana)
- [ ] Spell-System (Feuerbälle etc.) - [ ] Spell-System (Feuerbälle etc.)
- [ ] Schadenstypen (Physical, Fire, Ice, Lightning, Poison) - [ ] Schadenstypen (Physical, Fire, Ice, Lightning, Poison)
- [ ] Mehrere Gegnertypen - [ ] Mehrere Gegnertypen
@ -423,14 +471,13 @@ Charaktermodelle stammen von Mixamo (warrior.fbx) und werden mit separaten Anima
## Projektstruktur ## Projektstruktur
``` ```
DungeonCrawler/ DungeonCrawler/
├── assets/ # 3D-Modelle und Animationen ├── assets/
│ ├── models/ # Mixamo Charakter-Modelle (warrior.fbx + Texturen) │ ├── Warrior+Animation/ # Mixamo Charakter + Animationen (castle_guard_01.fbx + FBX)
│ ├── animations/ # Mixamo Animationen (Walking, Attack, etc.) │ └── kenney_animated-characters-1/ # Kenney Animated Characters Pack
│ └── kenney_blocky-characters_20/ # Kenney Block-Chars (nicht mehr aktiv)
├── classes/ # Klassen-Definitionen (.tres) ├── classes/ # Klassen-Definitionen (.tres)
│ ├── warrior.tres # Krieger (Ressource: NONE) │ ├── warrior.tres
│ ├── rogue.tres # Schurke (Ressource: ENERGY, 100) │ ├── rogue.tres
│ └── mage.tres # Magier (Ressource: MANA, 100) │ └── mage.tres
├── consumables/ # Verbrauchbare Items (.tres) ├── consumables/ # Verbrauchbare Items (.tres)
│ ├── small_hp_potion.tres │ ├── small_hp_potion.tres
│ └── small_mana_potion.tres │ └── small_mana_potion.tres
@ -439,49 +486,40 @@ DungeonCrawler/
│ ├── steel_sword.tres │ ├── steel_sword.tres
│ ├── leather_chest.tres │ ├── leather_chest.tres
│ ├── iron_helm.tres │ ├── iron_helm.tres
│ └── wooden_shield.tres │ ├── wooden_shield.tres
│ └── wooden_staff.tres
├── loot_tables/ # Loot-Tabellen (.tres) ├── loot_tables/ # Loot-Tabellen (.tres)
│ ├── goblin_loot.tres │ ├── goblin_loot.tres
│ └── skeleton_loot.tres │ └── skeleton_loot.tres
├── icons/ # Icons (SVG) ├── icons/ # Icons (SVG)
│ ├── autoattack_icon.svg │ ├── autoattack_icon.svg
│ ├── heavy_strike_icon.svg │ ├── heavy_strike_icon.svg
│ ├── frostbolt_icon.svg
│ ├── wand_icon.svg
│ ├── iron_sword_icon.svg │ ├── iron_sword_icon.svg
│ ├── steel_sword_icon.svg │ ├── steel_sword_icon.svg
│ ├── leather_chest_icon.svg │ ├── leather_chest_icon.svg
│ ├── iron_helm_icon.svg │ ├── iron_helm_icon.svg
│ ├── wooden_shield_icon.svg │ ├── wooden_shield_icon.svg
│ ├── wooden_staff_icon.svg
│ ├── hp_potion_icon.svg │ ├── hp_potion_icon.svg
│ ├── mana_potion_icon.svg │ └── mana_potion_icon.svg
│ ├── frostbolt_icon.svg ├── camera_pivot.gd # Kamera-Script (Lock-On, freies Drehen, Zoom)
│ ├── wand_icon.svg ├── character_class.gd # CharacterClass Resource
│ └── wooden_staff_icon.svg ├── character_panel.gd/.tscn # Charakter-Panel (Stats + Equipment)
├── camera_pivot.gd # Kamera-Script ├── class_selection_menu.gd/.tscn # Klassenauswahl
├── character_class.gd # CharacterClass Resource (inkl. ResourceType)
├── character_panel.gd # Charakter-Panel Script (Icon-Grid)
├── character_panel.tscn # Charakter-Panel Scene
├── class_selection_menu.gd # Klassenauswahl Script
├── class_selection_menu.tscn # Klassenauswahl Scene
├── consumable.gd # Consumable Resource ├── consumable.gd # Consumable Resource
├── enemy.gd # Gegner-Script ├── enemy.gd / enemy.tscn # Gegner (KI, Patrol, Kampf, Animationen)
├── enemy.tscn # Gegner-Scene
├── equipment.gd # Equipment Resource ├── equipment.gd # Equipment Resource
├── hud.gd # HUD-Script (inkl. ResourceBar) ├── hud.gd / hud.tscn # HUD (HP, Ressource, XP, Aktionsleiste, Castbar)
├── hud.tscn # HUD-Scene
├── inventory.gd # Inventar Resource ├── inventory.gd # Inventar Resource
├── inventory_panel.gd # Inventar-Panel Script ├── inventory_panel.gd/.tscn # Inventar-Panel (Drag & Drop)
├── inventory_panel.tscn # Inventar-Panel Scene
├── loot_entry.gd # LootEntry Resource ├── loot_entry.gd # LootEntry Resource
├── loot_table.gd # LootTable Resource ├── loot_table.gd # LootTable Resource
├── loot_window.gd # Loot-Fenster Script ├── loot_window.gd/.tscn # Loot-Fenster
├── loot_window.tscn # Loot-Fenster Scene ├── main_menu.gd/.tscn # Hauptmenü (Einstellungen)
├── main_menu.gd # Hauptmenü Script (Einstellungen) ├── player.gd / player.tscn # Spieler (Bewegung, Kampf, Skills, UI)
├── main_menu.tscn # Hauptmenü Scene ├── skill_panel.gd/.tscn # Fähigkeiten-Panel
├── player.gd # Spieler-Script (inkl. Ressourcen, Aktionsleiste) ├── world.gd / world.tscn # Hauptszene (Spawn, Respawn, Sky, Boden)
├── player.tscn # Spieler-Scene
├── skill_panel.gd # Fähigkeiten-Panel Script
├── skill_panel.tscn # Fähigkeiten-Panel Scene
├── world.gd # Welt-Script
├── world.tscn # Hauptszene
└── PROJEKTDOKU.md # Diese Dokumentation └── PROJEKTDOKU.md # Diese Dokumentation
``` ```

Binary file not shown.

View file

@ -0,0 +1,44 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bhkmifqviw8tn"
path="res://.godot/imported/Walking Turn 180.fbx-5e13de6275e7fa8075ec315bac8dc2b8.scn"
[deps]
source_file="res://assets/Warrior+Animation/Walking Turn 180.fbx"
dest_files=["res://.godot/imported/Walking Turn 180.fbx-5e13de6275e7fa8075ec315bac8dc2b8.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

Binary file not shown.

Binary file not shown.

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://bfn8s86o81t86" uid="uid://bfn8s86o81t86"
path="res://.godot/imported/left turn 90.fbx-7ede25625141e45b963a6f806ea7a4b6.scn" path="res://.godot/imported/Left Turn 90.fbx-28cd938b53e6490e956933e71aa8ff26.scn"
[deps] [deps]
source_file="res://assets/Warrior+Animation/left turn 90.fbx" source_file="res://assets/Warrior+Animation/Left Turn 90.fbx"
dest_files=["res://.godot/imported/left turn 90.fbx-7ede25625141e45b963a6f806ea7a4b6.scn"] dest_files=["res://.godot/imported/Left Turn 90.fbx-28cd938b53e6490e956933e71aa8ff26.scn"]
[params] [params]

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://bfg20q58h3ifm" uid="uid://bfg20q58h3ifm"
path="res://.godot/imported/right turn 90.fbx-1510f429e9c72d07d6b8f5bd0c243b9d.scn" path="res://.godot/imported/Right Turn 90.fbx-373084221b31914934f4218cfddc4307.scn"
[deps] [deps]
source_file="res://assets/Warrior+Animation/right turn 90.fbx" source_file="res://assets/Warrior+Animation/Right Turn 90.fbx"
dest_files=["res://.godot/imported/right turn 90.fbx-1510f429e9c72d07d6b8f5bd0c243b9d.scn"] dest_files=["res://.godot/imported/Right Turn 90.fbx-373084221b31914934f4218cfddc4307.scn"]
[params] [params]

Binary file not shown.

Binary file not shown.

View file

@ -39,14 +39,14 @@ func _input(event):
var has_target = player.target != null and is_instance_valid(player.target) var has_target = player.target != null and is_instance_valid(player.target)
if event is InputEventMouseMotion: if event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and not has_target: if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
# LMB: nur Kamera dreht sich, Spieler bleibt # LMB: nur Kamera dreht sich, Spieler bleibt (auch mit Target → umschauen)
world_yaw -= deg_to_rad(event.relative.x * sensitivity) world_yaw -= deg_to_rad(event.relative.x * sensitivity)
pitch -= event.relative.y * sensitivity pitch -= event.relative.y * sensitivity
pitch = clamp(pitch, min_pitch, max_pitch) pitch = clamp(pitch, min_pitch, max_pitch)
rotation_degrees.x = pitch rotation_degrees.x = pitch
elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) and not has_target: elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
# RMB: Spieler + Kamera drehen sich gemeinsam # RMB: Spieler + Kamera drehen sich gemeinsam (auch mit Target → weglaufen)
var delta_yaw = deg_to_rad(-event.relative.x * sensitivity) var delta_yaw = deg_to_rad(-event.relative.x * sensitivity)
world_yaw += delta_yaw world_yaw += delta_yaw
player.rotation.y += delta_yaw player.rotation.y += delta_yaw
@ -63,8 +63,11 @@ func _input(event):
func _process(delta): func _process(delta):
var player = get_parent() var player = get_parent()
if player.target != null and is_instance_valid(player.target) and not player.is_rolling: var rmb_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
var lmb_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
if player.target != null and is_instance_valid(player.target) and not player.is_rolling and not rmb_held and not lmb_held:
# Soft Lock-On: Spieler dreht sich zum Ziel, Kamera folgt direkt dahinter # Soft Lock-On: Spieler dreht sich zum Ziel, Kamera folgt direkt dahinter
# (RMB gehalten → Lock-On pausiert, Spieler kann sich wegdrehen)
var to_target = player.target.global_position - player.global_position var to_target = player.target.global_position - player.global_position
to_target.y = 0 to_target.y = 0
if to_target.length() > 0.1: if to_target.length() > 0.1:

238
enemy.gd
View file

@ -30,7 +30,7 @@ signal enemy_dropped_loot(loot: Dictionary, world_pos: Vector3)
@export var max_damage: int = 7 @export var max_damage: int = 7
@export var attack_range: float = 2.0 @export var attack_range: float = 2.0
@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 = 3.0 @export var move_speed: float = 5.5
@export var xp_reward: int = 20 @export var xp_reward: int = 20
@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
@ -42,20 +42,44 @@ var target = null # Spieler
# ZUSTAND # ZUSTAND
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
enum State { IDLE, CHASING, ATTACKING, DEAD } enum State { IDLE, PATROL, CHASING, ATTACKING, DEAD }
var state: State = State.IDLE var state: State = State.IDLE
var attack_cooldown: float = 0.0 var attack_cooldown: float = 0.0
var is_dead: bool = false var is_dead: bool = false
# Patrol
@export var patrol_radius: float = 8.0
@export var patrol_speed: float = 1.5 # Laufgeschwindigkeit beim Patrouillieren
var spawn_position: Vector3 = Vector3.ZERO
var patrol_target: Vector3 = Vector3.ZERO
var patrol_wait_timer: float = 0.0
const GRAVITY = 9.8 const GRAVITY = 9.8
# ═══════════════════════════════════════════════════════════════
# ANIMATION
# ═══════════════════════════════════════════════════════════════
const ANIM_IDLE = "res://assets/Warrior+Animation/idle.fbx"
const ANIM_WALK = "res://assets/Warrior+Animation/walking.fbx"
const ANIM_RUN = "res://assets/Warrior+Animation/running.fbx"
const ANIM_AUTOATTACK = "res://assets/Warrior+Animation/Autoattack.fbx"
const ANIM_DEATH = "res://assets/Warrior+Animation/Dying Backwards.fbx"
const ANIM_TURN_LEFT = "res://assets/Warrior+Animation/Left Turn 90.fbx"
const ANIM_TURN_RIGHT = "res://assets/Warrior+Animation/Right Turn 90.fbx"
var anim_player: AnimationPlayer = null
var current_anim: String = ""
var is_turning: bool = false
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# NODE-REFERENZEN # NODE-REFERENZEN
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
@onready var health_label: Label3D = $HealthDisplay/Label3D @onready var health_label: Label3D = $HealthDisplay/Label3D
@onready var model: Node3D = $Model
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# READY # READY
@ -70,6 +94,98 @@ func _ready():
nav_agent.path_desired_distance = 0.5 nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = attack_range * 0.9 nav_agent.target_desired_distance = attack_range * 0.9
# Animationen einrichten
_setup_animations()
# Spawnpunkt erst nach dem ersten Frame setzen (global_position ist in _ready() noch nicht final)
await get_tree().process_frame
spawn_position = global_position
_pick_patrol_target()
patrol_wait_timer = 5.0 # 5 Sekunden idle nach Spawn
state = State.PATROL
# ═══════════════════════════════════════════════════════════════
# ANIMATIONEN
# ═══════════════════════════════════════════════════════════════
func _setup_animations():
if model:
anim_player = model.find_child("AnimationPlayer", true, false) as AnimationPlayer
if anim_player == null:
push_warning("Enemy: Kein AnimationPlayer gefunden!")
return
# FBX-Import-Animationen komplett aufräumen
anim_player.stop()
anim_player.autoplay = ""
# Alle vorhandenen Libraries entfernen und frische erstellen
for lib_name in anim_player.get_animation_library_list():
anim_player.remove_animation_library(lib_name)
anim_player.add_animation_library("", AnimationLibrary.new())
_load_anim_from_fbx(ANIM_IDLE, "idle", true)
_load_anim_from_fbx(ANIM_WALK, "walk", true)
_load_anim_from_fbx(ANIM_RUN, "run", true)
_load_anim_from_fbx(ANIM_AUTOATTACK, "autoattack", false)
_load_anim_from_fbx(ANIM_DEATH, "death", false)
_load_anim_from_fbx(ANIM_TURN_LEFT, "turn_left", false)
_load_anim_from_fbx(ANIM_TURN_RIGHT, "turn_right", false)
anim_player.animation_finished.connect(_on_animation_finished)
_play_anim("idle")
func _load_anim_from_fbx(fbx_path: String, anim_name: String, loop: bool = false):
var scene = load(fbx_path)
if scene == null:
push_warning("Enemy: FBX nicht geladen: " + fbx_path)
return
var instance = scene.instantiate()
var ext_ap = instance.find_child("AnimationPlayer", true, false) as AnimationPlayer
if ext_ap == null:
instance.queue_free()
push_warning("Enemy: Kein AnimationPlayer in " + fbx_path)
return
var anim_list = ext_ap.get_animation_list()
if anim_list.is_empty():
instance.queue_free()
return
var anim = ext_ap.get_animation(anim_list[0]).duplicate(true)
_strip_root_motion(anim)
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
var lib = anim_player.get_animation_library("")
if not lib.has_animation(anim_name):
lib.add_animation(anim_name, anim)
instance.queue_free()
func _strip_root_motion(anim: Animation):
for i in range(anim.get_track_count() - 1, -1, -1):
if anim.track_get_type(i) != Animation.TYPE_POSITION_3D:
continue
var np: NodePath = anim.track_get_path(i)
if np.get_subname_count() == 0:
# Node-Position-Track (Armature-Root) → komplett entfernen
anim.remove_track(i)
else:
# Knochen-Position: ALLE XZ-Werte nullen, Y behalten
var key_count = anim.track_get_key_count(i)
for k in range(key_count):
var v: Vector3 = anim.track_get_key_value(i, k)
anim.track_set_key_value(i, k, Vector3(0.0, v.y, 0.0))
func _play_anim(anim_name: String):
if anim_player == null:
return
if anim_name != current_anim:
current_anim = anim_name
if anim_player.has_animation(anim_name):
anim_player.play(anim_name)
func _on_animation_finished(anim_name: StringName):
if anim_name == "autoattack":
_play_anim("idle")
elif anim_name == "turn_left" or anim_name == "turn_right":
is_turning = false
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# PHYSICS PROCESS # PHYSICS PROCESS
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@ -86,11 +202,24 @@ func _physics_process(delta):
if attack_cooldown > 0: if attack_cooldown > 0:
attack_cooldown -= delta attack_cooldown -= delta
# Kein Ziel → Idle # Kein Ziel → Spieler suchen
if target == null or not is_instance_valid(target): if target == null or not is_instance_valid(target):
state = State.IDLE target = null
velocity.x = 0 if state == State.CHASING or state == State.ATTACKING:
velocity.z = 0 state = State.PATROL
_pick_patrol_target()
# Spieler in Reichweite? → Aggro
var players = get_tree().get_nodes_in_group("player")
if players.size() > 0:
var player = players[0]
var dist = global_position.distance_to(player.global_position)
if dist <= detection_range:
target = player
state = State.CHASING
else:
_do_patrol(delta)
else:
_do_patrol(delta)
move_and_slide() move_and_slide()
return return
@ -98,15 +227,22 @@ func _physics_process(delta):
match state: match state:
State.IDLE: State.IDLE:
_play_anim("idle")
if distance <= detection_range: if distance <= detection_range:
state = State.CHASING state = State.CHASING
State.PATROL:
if distance <= detection_range:
state = State.CHASING
else:
_do_patrol(delta)
State.CHASING: State.CHASING:
if distance <= attack_range: if distance <= attack_range:
state = State.ATTACKING state = State.ATTACKING
velocity.x = 0 velocity.x = 0
velocity.z = 0 velocity.z = 0
else: else:
_move_toward_target() _chase_target()
_play_anim("run")
State.ATTACKING: State.ATTACKING:
if distance > attack_range * 1.5: if distance > attack_range * 1.5:
state = State.CHASING state = State.CHASING
@ -116,22 +252,84 @@ func _physics_process(delta):
_face_target() _face_target()
if attack_cooldown <= 0: if attack_cooldown <= 0:
_perform_attack() _perform_attack()
elif current_anim != "autoattack":
_play_anim("idle")
move_and_slide() move_and_slide()
func _move_toward_target(): func _chase_target():
if target == null: if target == null:
return return
nav_agent.target_position = target.global_position var direction = (target.global_position - global_position)
if nav_agent.is_navigation_finished():
return
var next_pos = nav_agent.get_next_path_position()
var direction = (next_pos - global_position).normalized()
direction.y = 0 direction.y = 0
if direction.length() < 0.1:
return
direction = direction.normalized()
velocity.x = direction.x * move_speed velocity.x = direction.x * move_speed
velocity.z = direction.z * move_speed velocity.z = direction.z * move_speed
_face_direction(direction) _face_direction(direction)
func _do_patrol(delta: float):
# Turn-Animation läuft → warten
if is_turning:
velocity.x = 0
velocity.z = 0
return
# Warten zwischen Patrol-Punkten
if patrol_wait_timer > 0:
patrol_wait_timer -= delta
velocity.x = 0
velocity.z = 0
_play_anim("idle")
if patrol_wait_timer <= 0:
# Wartezeit vorbei → Turn-Animation zum nächsten Ziel abspielen
_turn_toward_patrol_target()
return
# Zum Patrol-Ziel laufen
var dist = global_position.distance_to(patrol_target)
if dist <= 1.0:
# Ziel erreicht → kurz warten, neues Ziel
velocity.x = 0
velocity.z = 0
_play_anim("idle")
patrol_wait_timer = randf_range(2.0, 5.0)
_pick_patrol_target()
return
var direction = (patrol_target - global_position)
direction.y = 0
direction = direction.normalized()
velocity.x = direction.x * patrol_speed
velocity.z = direction.z * patrol_speed
_face_direction(direction)
_play_anim("walk")
func _turn_toward_patrol_target():
var dir = (patrol_target - global_position)
dir.y = 0
if dir.length() < 0.1:
return
dir = dir.normalized()
# Winkel zwischen aktueller Blickrichtung und Zielrichtung berechnen
var forward = Vector3(sin(rotation.y), 0, cos(rotation.y))
var cross = forward.cross(dir).y # positiv = Ziel rechts, negativ = Ziel links
is_turning = true
if cross >= 0:
_play_anim("turn_right")
else:
_play_anim("turn_left")
# Schon mal in die Zielrichtung drehen
_face_direction(dir)
func _pick_patrol_target():
var angle = randf() * TAU
var dist = randf_range(3.0, patrol_radius)
patrol_target = spawn_position + Vector3(cos(angle) * dist, 0, sin(angle) * dist)
func _face_target(): func _face_target():
if target == null: if target == null:
return return
@ -154,6 +352,7 @@ func _perform_attack():
var damage = randi_range(min_damage, max_damage) var damage = randi_range(min_damage, max_damage)
target.take_damage(damage) target.take_damage(damage)
attack_cooldown = attack_speed attack_cooldown = attack_speed
_play_anim("autoattack")
print(name + " greift an: " + str(damage) + " Schaden") print(name + " greift an: " + str(damage) + " Schaden")
func take_damage(amount: int): func take_damage(amount: int):
@ -161,6 +360,12 @@ func take_damage(amount: int):
return return
current_hp = clamp(current_hp - amount, 0, max_hp) current_hp = clamp(current_hp - amount, 0, max_hp)
_update_health_display() _update_health_display()
# Aggro: Bei Schaden sofort den Spieler verfolgen
if state == State.IDLE or state == State.PATROL:
var players = get_tree().get_nodes_in_group("player")
if players.size() > 0:
target = players[0]
state = State.CHASING
if current_hp <= 0: if current_hp <= 0:
_die() _die()
@ -185,9 +390,12 @@ func _update_health_display():
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
func _die(): func _die():
if is_dead:
return
is_dead = true is_dead = true
state = State.DEAD state = State.DEAD
velocity = Vector3.ZERO velocity = Vector3.ZERO
_play_anim("death")
print(name + " gestorben!") print(name + " gestorben!")
# Loot generieren # Loot generieren
@ -195,11 +403,11 @@ func _die():
var loot = loot_table.generate_loot() var loot = loot_table.generate_loot()
enemy_dropped_loot.emit(loot, global_position) enemy_dropped_loot.emit(loot, global_position)
# XP und Respawn-Signal # XP und Respawn-Signal (nur einmal!)
enemy_died.emit(global_position, xp_reward) enemy_died.emit(global_position, xp_reward)
# Kollision deaktivieren und Node entfernen # Kollision deaktivieren und Node entfernen
set_deferred("collision_layer", 0) set_deferred("collision_layer", 0)
set_deferred("collision_mask", 0) set_deferred("collision_mask", 0)
await get_tree().create_timer(1.5).timeout await get_tree().create_timer(2.0).timeout
queue_free() queue_free()

View file

@ -1,26 +1,28 @@
[gd_scene format=3 uid="uid://cvojaeanxugfj"] [gd_scene format=3 uid="uid://cvojaeanxugfj"]
[ext_resource type="Script" path="res://enemy.gd" id="1_enemy"] [ext_resource type="Script" uid="uid://gaqwoakxyhet" path="res://enemy.gd" id="1_enemy"]
[ext_resource type="PackedScene" uid="uid://daeym1tdcnhhd" path="res://assets/Warrior+Animation/castle_guard_01.fbx" id="2_model"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_7k104"]
radius = 0.4 height = 2.208252
height = 1.8
[node name="Enemy" type="CharacterBody3D"] [node name="Enemy" type="CharacterBody3D" unique_id=393882142]
script = ExtResource("1_enemy") script = ExtResource("1_enemy")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="Model" parent="." unique_id=842107644 instance=ExtResource("2_model")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1531674298]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.010422, 0)
shape = SubResource("CapsuleShape3D_7k104")
[node name="HealthDisplay" type="Node3D" parent="."] [node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=838601477]
[node name="HealthDisplay" type="Node3D" parent="." unique_id=1341862509]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
[node name="Label3D" type="Label3D" parent="HealthDisplay"] [node name="Label3D" type="Label3D" parent="HealthDisplay" unique_id=80852959]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1362207, 0)
pixel_size = 0.01 pixel_size = 0.01
billboard = 3
text = "50 / 50" text = "50 / 50"
font_size = 64 font_size = 24
outline_size = 8 outline_size = 8

View file

@ -152,6 +152,7 @@ const ROLL_COOLDOWN: float = 1.5
# wird nur aktualisiert wenn LMB NICHT gedrückt ist, damit # wird nur aktualisiert wenn LMB NICHT gedrückt ist, damit
# LMB-Kamerarotation die Laufrichtung nicht verändert # LMB-Kamerarotation die Laufrichtung nicht verändert
var _movement_yaw: float = 0.0 var _movement_yaw: float = 0.0
var _lmb_press_pos: Vector2 = Vector2.ZERO
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# NODE-REFERENZEN # NODE-REFERENZEN
@ -173,6 +174,8 @@ var character_panel = null
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
func _ready(): func _ready():
add_to_group("player")
# Jolt Physics: Boden sicher erkennen # Jolt Physics: Boden sicher erkennen
floor_snap_length = 0.3 floor_snap_length = 0.3
floor_max_angle = deg_to_rad(50.0) floor_max_angle = deg_to_rad(50.0)
@ -696,6 +699,13 @@ func set_target(new_target, start_attack: bool = false):
if start_attack: if start_attack:
start_autoattack() start_autoattack()
func clear_target():
if target != null and is_instance_valid(target):
target.hide_health()
target = null
autoattack_active = false
print("Ziel aufgehoben")
func _try_select_target(start_attack: bool = false): func _try_select_target(start_attack: bool = false):
var space_state = get_world_3d().direct_space_state var space_state = get_world_3d().direct_space_state
var viewport = get_viewport() var viewport = get_viewport()
@ -707,6 +717,9 @@ 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)
else:
# Klick auf freie Fläche → Target entfernen
clear_target()
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# ANIMATION SETUP # ANIMATION SETUP
@ -889,8 +902,8 @@ func _physics_process(delta):
for key in consumable_cooldowns.keys(): for key in consumable_cooldowns.keys():
consumable_cooldowns[key] = max(0.0, consumable_cooldowns[key] - delta) consumable_cooldowns[key] = max(0.0, consumable_cooldowns[key] - delta)
# ── Autoattack nach GCD ─────────────────────────────────── # ── Autoattack nach GCD oder wenn in Range ───────────────
if gcd_was_active and global_cooldown <= 0 and autoattack_active: if autoattack_active and global_cooldown <= 0 and not is_casting:
perform_autoattack() perform_autoattack()
# ── Cast-System ─────────────────────────────────────────── # ── Cast-System ───────────────────────────────────────────
@ -921,9 +934,13 @@ func _physics_process(delta):
if is_casting: if is_casting:
_cancel_cast() _cancel_cast()
# ── Zielauswahl ─────────────────────────────────────────── # ── Zielauswahl (nur Klick, nicht Drag) ───────────────────
if Input.is_action_just_pressed("select_target"): if Input.is_action_just_pressed("select_target"):
_try_select_target(false) _lmb_press_pos = get_viewport().get_mouse_position()
if Input.is_action_just_released("select_target"):
var release_pos = get_viewport().get_mouse_position()
if _lmb_press_pos.distance_to(release_pos) < 5.0:
_try_select_target(false)
if Input.is_action_just_pressed("ui_right_mouse"): if Input.is_action_just_pressed("ui_right_mouse"):
_try_select_target(true) _try_select_target(true)

View file

@ -7,13 +7,13 @@
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.4 radius = 0.4
height = 1.8 height = 2.088965
[node name="Player" type="CharacterBody3D" unique_id=1565111917] [node name="Player" type="CharacterBody3D" unique_id=1565111917]
script = ExtResource("1_player") script = ExtResource("1_player")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=481888033] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=481888033]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0444825, 0)
shape = SubResource("CapsuleShape3D_1") shape = SubResource("CapsuleShape3D_1")
[node name="Model" type="Node3D" parent="." unique_id=297754421] [node name="Model" type="Node3D" parent="." unique_id=297754421]

View file

@ -111,8 +111,9 @@ func _on_class_selected(character_class: CharacterClass):
print("Klasse gewählt: ", character_class.class_name_de) print("Klasse gewählt: ", character_class.class_name_de)
# Jetzt Gegner initialisieren # Jetzt Gegner initialisieren
var enemy = get_node("Enemy") for child in get_children():
_setup_enemy(enemy) if child.has_method("take_damage") and child != player:
_setup_enemy(child)
# Gegner initialisieren und Signal verbinden # Gegner initialisieren und Signal verbinden
func _setup_enemy(enemy): func _setup_enemy(enemy):
@ -120,8 +121,10 @@ func _setup_enemy(enemy):
enemy.target = player enemy.target = player
if enemy.loot_table == null: if enemy.loot_table == null:
enemy.loot_table = GOBLIN_LOOT enemy.loot_table = GOBLIN_LOOT
enemy.enemy_died.connect(_on_enemy_died) if not enemy.enemy_died.is_connected(_on_enemy_died):
enemy.enemy_dropped_loot.connect(_on_enemy_dropped_loot) enemy.enemy_died.connect(_on_enemy_died)
if not enemy.enemy_dropped_loot.is_connected(_on_enemy_dropped_loot):
enemy.enemy_dropped_loot.connect(_on_enemy_dropped_loot)
else: else:
print("Fehler: Player oder Enemy nicht gefunden!") print("Fehler: Player oder Enemy nicht gefunden!")
@ -131,7 +134,9 @@ func _on_enemy_dropped_loot(loot: Dictionary, world_pos: Vector3):
player.receive_loot(loot, world_pos) player.receive_loot(loot, world_pos)
# Gegner gestorben: Nach 5 Sekunden respawnen # Gegner gestorben: Nach 5 Sekunden respawnen
func _on_enemy_died(spawn_position: Vector3, _xp_reward: int): func _on_enemy_died(spawn_position: Vector3, xp_reward: int):
if player:
player.gain_xp(xp_reward)
print("Respawn in ", RESPAWN_TIME, " Sekunden...") print("Respawn in ", RESPAWN_TIME, " Sekunden...")
await get_tree().create_timer(RESPAWN_TIME).timeout await get_tree().create_timer(RESPAWN_TIME).timeout
_spawn_enemy(spawn_position) _spawn_enemy(spawn_position)

View file

@ -2,6 +2,7 @@
[ext_resource type="PackedScene" uid="uid://dniyuebl8yhtv" path="res://player.tscn" id="1_f3sb7"] [ext_resource type="PackedScene" uid="uid://dniyuebl8yhtv" path="res://player.tscn" id="1_f3sb7"]
[ext_resource type="Script" uid="uid://cx56h588mfsk0" path="res://world.gd" id="1_tlwt5"] [ext_resource type="Script" uid="uid://cx56h588mfsk0" path="res://world.gd" id="1_tlwt5"]
[ext_resource type="PackedScene" uid="uid://cvojaeanxugfj" path="res://enemy.tscn" id="3_enemy"]
[sub_resource type="BoxShape3D" id="BoxShape3D_fj7yv"] [sub_resource type="BoxShape3D" id="BoxShape3D_fj7yv"]
size = Vector3(200, 0.5, 200) size = Vector3(200, 0.5, 200)
@ -24,10 +25,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
mesh = SubResource("BoxMesh_tlwt5") mesh = SubResource("BoxMesh_tlwt5")
[node name="Player" parent="." unique_id=937297102 instance=ExtResource("1_f3sb7")] [node name="Player" parent="." unique_id=937297102 instance=ExtResource("1_f3sb7")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4345045, 0)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1394887598] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1394887598]
transform = Transform3D(-45, 0, 0, 0, -45, 0, 0, 0, -45, 0, 0, 0) transform = Transform3D(-45, 0, 0, 0, -45, 0, 0, 0, -45, 0, 0, 0)
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=827244005] [node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=827244005]
navigation_mesh = SubResource("NavigationMesh_fj7yv") navigation_mesh = SubResource("NavigationMesh_fj7yv")
[node name="enemy" parent="." unique_id=393882142 instance=ExtResource("3_enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.38837337, -35.95893)