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:
parent
752086bd1b
commit
e4efb239f2
24 changed files with 473 additions and 152 deletions
240
PROJEKTDOKU.md
240
PROJEKTDOKU.md
|
|
@ -20,15 +20,17 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge
|
|||
|---|---|
|
||||
| W / A / S / D | Bewegen |
|
||||
| Mausrad | Kamera zoomen (min 5, max 20) |
|
||||
| RMB gehalten | Kamera drehen, Spieler schaut mit |
|
||||
| Linksklick auf Gegner | Ziel markieren |
|
||||
| Rechtsklick auf Gegner | Ziel markieren + Autoattack starten |
|
||||
| LMB gehalten + Mausbewegung | Kamera frei drehen (umschauen), Charakter bleibt in Position |
|
||||
| LMB Klick auf Gegner | Ziel markieren |
|
||||
| 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) |
|
||||
| C | Charakter-Panel (Stats + Equipment) |
|
||||
| I | Inventar öffnen/schließen |
|
||||
| P | Fähigkeiten-Panel (alle Skills, Drag auf Aktionsleiste) |
|
||||
| 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)
|
||||
├── Player (player.tscn)
|
||||
├── Enemy (enemy.tscn)
|
||||
├── StaticBody3D (Boden)
|
||||
│ ├── MeshInstance3D
|
||||
├── Boden (StaticBody3D)
|
||||
│ ├── MeshInstance3D (Schachbrett-Shader)
|
||||
│ └── CollisionShape3D
|
||||
├── DirectionalLight3D
|
||||
└── NavigationRegion3D
|
||||
|
|
@ -51,31 +53,43 @@ World (Node3D)
|
|||
Der Spielercharakter mit allen UI-Panels.
|
||||
```
|
||||
Player (CharacterBody3D)
|
||||
├── PlayerModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer)
|
||||
├── Model (Node3D)
|
||||
│ └── castle_guard_01 (Mixamo FBX mit Skeleton3D + AnimationPlayer)
|
||||
├── CollisionShape3D
|
||||
├── CameraPivot (Node3D)
|
||||
├── CameraPivot (Node3D + camera_pivot.gd)
|
||||
│ └── Camera3D
|
||||
├── HUD (hud.tscn)
|
||||
├── CharacterPanel (character_panel.tscn)
|
||||
├── InventoryPanel (inventory_panel.tscn)
|
||||
├── LootWindow (loot_window.tscn)
|
||||
└── SkillPanel (skill_panel.tscn)
|
||||
└── HUD (hud.tscn)
|
||||
```
|
||||
UI-Panels (CharacterPanel, InventoryPanel, LootWindow, SkillPanel) werden zur Laufzeit erstellt.
|
||||
|
||||
### enemy.tscn
|
||||
Ein Gegner mit Level-basiertem Stats-System.
|
||||
Ein Gegner mit Patrol-KI und Kampf-Animationen.
|
||||
```
|
||||
Enemy (CharacterBody3D)
|
||||
├── EnemyModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer)
|
||||
├── Model (castle_guard_01.fbx — Mixamo Charakter mit Skeleton3D + AnimationPlayer)
|
||||
├── CollisionShape3D
|
||||
├── Area3D
|
||||
│ └── CollisionShape3D
|
||||
├── 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
|
||||
|
||||
### CharacterClass (character_class.gd)
|
||||
|
|
@ -133,9 +147,10 @@ Die Ressourcen-Leiste wird nur angezeigt wenn die Klasse eine Ressource hat.
|
|||
|
||||
## 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
|
||||
- **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
|
||||
|
||||
---
|
||||
|
|
@ -230,6 +245,8 @@ Resource-Klasse für das Spieler-Inventar.
|
|||
| add_gold(amount) | Gold hinzufügen |
|
||||
| spend_gold(amount) | Gold ausgeben (false wenn nicht genug) |
|
||||
| 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)
|
||||
- 5x4 Grid mit Item-Slots
|
||||
|
|
@ -237,6 +254,7 @@ Resource-Klasse für das Spieler-Inventar.
|
|||
- Items in Seltenheitsfarbe
|
||||
- Tooltips mit vollständigen Item-Stats
|
||||
- **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%`
|
||||
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 = (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
|
||||
- **Cooldown:** GCD (Waffen-Attackspeed / Haste)
|
||||
- **Reichweite:** Waffen-Reichweite oder 3.0 (unbewaffnet)
|
||||
- Automatisch per Rechtsklick oder manuell per Taste 1
|
||||
- **Animation:** autoattack
|
||||
|
||||
#### 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
|
||||
- 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
|
||||
- Icons werden beim Spielstart geladen
|
||||
- Cooldown-Anzeige: Dunkle Überlagerung + verbleibende Zeit
|
||||
- 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)
|
||||
|
||||
### Stats
|
||||
Level-basiert mit automatischer Skalierung:
|
||||
|
||||
| Stat | Formel |
|
||||
### Stats (Export-Variablen)
|
||||
| Stat | Standard |
|
||||
|---|---|
|
||||
| Stärke | base_strength + (level-1) * 2 |
|
||||
| Ausdauer | base_stamina + (level-1) * 3 |
|
||||
| Rüstung | base_armor + (level-1) * 2 |
|
||||
| HP | Ausdauer * 10 |
|
||||
| Schaden | Stärke * 0.5 + 2 |
|
||||
| XP-Belohnung | 25 * Level |
|
||||
| max_hp | 50 |
|
||||
| min_damage / max_damage | 3-7 |
|
||||
| attack_range | 2.0 |
|
||||
| attack_speed | 2.0s |
|
||||
| move_speed | 5.5 (Rennen bei Aggro) |
|
||||
| patrol_speed | 1.5 (Laufen bei Patrol) |
|
||||
| detection_range | 15.0 |
|
||||
| patrol_radius | 8.0 |
|
||||
| xp_reward | 20 |
|
||||
|
||||
### KI-Verhalten (State Machine)
|
||||
| State | Beschreibung |
|
||||
|---|---|
|
||||
| PATROL | Zufällig im Radius um Spawn-Position herumlaufen |
|
||||
| CHASE | Spieler verfolgen (Aggro-Range: 8.0) |
|
||||
| ATTACK | Angreifen wenn in Reichweite (1.5) |
|
||||
| IDLE | Wartet (wird initial für 5s nach Spawn verwendet) |
|
||||
| PATROL | Läuft zwischen zufälligen Punkten im Spawn-Radius, walk-Animation, Turn-Animationen beim Richtungswechsel |
|
||||
| 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
|
||||
- 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
|
||||
- Jeder Gegner hat eine optionale `loot_table` (LootTable Resource)
|
||||
- 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 |
|
||||
| GoldLabel | Gold-Anzeige in Goldfarbe |
|
||||
| ActionBar | 9 Slots mit Icons, Cooldowns, Klick-Support, Stack-Anzeige für Consumables |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
| Castbar | Zauberbalken über der Aktionsleiste (eigener CanvasLayer, layer 10) |
|
||||
|
||||
---
|
||||
|
||||
## Geplante Features
|
||||
- [ ] Wut-Ressource für Krieger
|
||||
- [ ] Ressourcen-System für Gegner (nicht alle haben Mana)
|
||||
- [ ] Spell-System (Feuerbälle etc.)
|
||||
- [ ] Schadenstypen (Physical, Fire, Ice, Lightning, Poison)
|
||||
- [ ] Mehrere Gegnertypen
|
||||
|
|
@ -423,14 +471,13 @@ Charaktermodelle stammen von Mixamo (warrior.fbx) und werden mit separaten Anima
|
|||
## Projektstruktur
|
||||
```
|
||||
DungeonCrawler/
|
||||
├── assets/ # 3D-Modelle und Animationen
|
||||
│ ├── models/ # Mixamo Charakter-Modelle (warrior.fbx + Texturen)
|
||||
│ ├── animations/ # Mixamo Animationen (Walking, Attack, etc.)
|
||||
│ └── kenney_blocky-characters_20/ # Kenney Block-Chars (nicht mehr aktiv)
|
||||
├── assets/
|
||||
│ ├── Warrior+Animation/ # Mixamo Charakter + Animationen (castle_guard_01.fbx + FBX)
|
||||
│ └── kenney_animated-characters-1/ # Kenney Animated Characters Pack
|
||||
├── classes/ # Klassen-Definitionen (.tres)
|
||||
│ ├── warrior.tres # Krieger (Ressource: NONE)
|
||||
│ ├── rogue.tres # Schurke (Ressource: ENERGY, 100)
|
||||
│ └── mage.tres # Magier (Ressource: MANA, 100)
|
||||
│ ├── warrior.tres
|
||||
│ ├── rogue.tres
|
||||
│ └── mage.tres
|
||||
├── consumables/ # Verbrauchbare Items (.tres)
|
||||
│ ├── small_hp_potion.tres
|
||||
│ └── small_mana_potion.tres
|
||||
|
|
@ -439,49 +486,40 @@ DungeonCrawler/
|
|||
│ ├── steel_sword.tres
|
||||
│ ├── leather_chest.tres
|
||||
│ ├── iron_helm.tres
|
||||
│ └── wooden_shield.tres
|
||||
│ ├── wooden_shield.tres
|
||||
│ └── wooden_staff.tres
|
||||
├── loot_tables/ # Loot-Tabellen (.tres)
|
||||
│ ├── goblin_loot.tres
|
||||
│ └── skeleton_loot.tres
|
||||
├── icons/ # Icons (SVG)
|
||||
│ ├── autoattack_icon.svg
|
||||
│ ├── heavy_strike_icon.svg
|
||||
│ ├── frostbolt_icon.svg
|
||||
│ ├── wand_icon.svg
|
||||
│ ├── iron_sword_icon.svg
|
||||
│ ├── steel_sword_icon.svg
|
||||
│ ├── leather_chest_icon.svg
|
||||
│ ├── iron_helm_icon.svg
|
||||
│ ├── wooden_shield_icon.svg
|
||||
│ ├── wooden_staff_icon.svg
|
||||
│ ├── hp_potion_icon.svg
|
||||
│ ├── mana_potion_icon.svg
|
||||
│ ├── frostbolt_icon.svg
|
||||
│ ├── wand_icon.svg
|
||||
│ └── wooden_staff_icon.svg
|
||||
├── camera_pivot.gd # Kamera-Script
|
||||
├── 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
|
||||
│ └── mana_potion_icon.svg
|
||||
├── camera_pivot.gd # Kamera-Script (Lock-On, freies Drehen, Zoom)
|
||||
├── character_class.gd # CharacterClass Resource
|
||||
├── character_panel.gd/.tscn # Charakter-Panel (Stats + Equipment)
|
||||
├── class_selection_menu.gd/.tscn # Klassenauswahl
|
||||
├── consumable.gd # Consumable Resource
|
||||
├── enemy.gd # Gegner-Script
|
||||
├── enemy.tscn # Gegner-Scene
|
||||
├── enemy.gd / enemy.tscn # Gegner (KI, Patrol, Kampf, Animationen)
|
||||
├── equipment.gd # Equipment Resource
|
||||
├── hud.gd # HUD-Script (inkl. ResourceBar)
|
||||
├── hud.tscn # HUD-Scene
|
||||
├── hud.gd / hud.tscn # HUD (HP, Ressource, XP, Aktionsleiste, Castbar)
|
||||
├── inventory.gd # Inventar Resource
|
||||
├── inventory_panel.gd # Inventar-Panel Script
|
||||
├── inventory_panel.tscn # Inventar-Panel Scene
|
||||
├── inventory_panel.gd/.tscn # Inventar-Panel (Drag & Drop)
|
||||
├── loot_entry.gd # LootEntry Resource
|
||||
├── loot_table.gd # LootTable Resource
|
||||
├── loot_window.gd # Loot-Fenster Script
|
||||
├── loot_window.tscn # Loot-Fenster Scene
|
||||
├── main_menu.gd # Hauptmenü Script (Einstellungen)
|
||||
├── main_menu.tscn # Hauptmenü Scene
|
||||
├── player.gd # Spieler-Script (inkl. Ressourcen, Aktionsleiste)
|
||||
├── 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
|
||||
├── loot_window.gd/.tscn # Loot-Fenster
|
||||
├── main_menu.gd/.tscn # Hauptmenü (Einstellungen)
|
||||
├── player.gd / player.tscn # Spieler (Bewegung, Kampf, Skills, UI)
|
||||
├── skill_panel.gd/.tscn # Fähigkeiten-Panel
|
||||
├── world.gd / world.tscn # Hauptszene (Spawn, Respawn, Sky, Boden)
|
||||
└── PROJEKTDOKU.md # Diese Dokumentation
|
||||
```
|
||||
|
|
|
|||
BIN
assets/Warrior+Animation/Walking Turn 180.fbx
Normal file
BIN
assets/Warrior+Animation/Walking Turn 180.fbx
Normal file
Binary file not shown.
44
assets/Warrior+Animation/Walking Turn 180.fbx.import
Normal file
44
assets/Warrior+Animation/Walking Turn 180.fbx.import
Normal 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -4,12 +4,12 @@ importer="scene"
|
|||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bfn8s86o81t86"
|
||||
path="res://.godot/imported/left turn 90.fbx-7ede25625141e45b963a6f806ea7a4b6.scn"
|
||||
path="res://.godot/imported/Left Turn 90.fbx-28cd938b53e6490e956933e71aa8ff26.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Warrior+Animation/left turn 90.fbx"
|
||||
dest_files=["res://.godot/imported/left turn 90.fbx-7ede25625141e45b963a6f806ea7a4b6.scn"]
|
||||
source_file="res://assets/Warrior+Animation/Left Turn 90.fbx"
|
||||
dest_files=["res://.godot/imported/Left Turn 90.fbx-28cd938b53e6490e956933e71aa8ff26.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -4,12 +4,12 @@ importer="scene"
|
|||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bfg20q58h3ifm"
|
||||
path="res://.godot/imported/right turn 90.fbx-1510f429e9c72d07d6b8f5bd0c243b9d.scn"
|
||||
path="res://.godot/imported/Right Turn 90.fbx-373084221b31914934f4218cfddc4307.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Warrior+Animation/right turn 90.fbx"
|
||||
dest_files=["res://.godot/imported/right turn 90.fbx-1510f429e9c72d07d6b8f5bd0c243b9d.scn"]
|
||||
source_file="res://assets/Warrior+Animation/Right Turn 90.fbx"
|
||||
dest_files=["res://.godot/imported/Right Turn 90.fbx-373084221b31914934f4218cfddc4307.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -39,14 +39,14 @@ func _input(event):
|
|||
var has_target = player.target != null and is_instance_valid(player.target)
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and not has_target:
|
||||
# LMB: nur Kamera dreht sich, Spieler bleibt
|
||||
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||||
# LMB: nur Kamera dreht sich, Spieler bleibt (auch mit Target → umschauen)
|
||||
world_yaw -= deg_to_rad(event.relative.x * sensitivity)
|
||||
pitch -= event.relative.y * sensitivity
|
||||
pitch = clamp(pitch, min_pitch, max_pitch)
|
||||
rotation_degrees.x = pitch
|
||||
elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) and not has_target:
|
||||
# RMB: Spieler + Kamera drehen sich gemeinsam
|
||||
elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
|
||||
# RMB: Spieler + Kamera drehen sich gemeinsam (auch mit Target → weglaufen)
|
||||
var delta_yaw = deg_to_rad(-event.relative.x * sensitivity)
|
||||
world_yaw += delta_yaw
|
||||
player.rotation.y += delta_yaw
|
||||
|
|
@ -63,8 +63,11 @@ func _input(event):
|
|||
func _process(delta):
|
||||
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
|
||||
# (RMB gehalten → Lock-On pausiert, Spieler kann sich wegdrehen)
|
||||
var to_target = player.target.global_position - player.global_position
|
||||
to_target.y = 0
|
||||
if to_target.length() > 0.1:
|
||||
|
|
|
|||
238
enemy.gd
238
enemy.gd
|
|
@ -30,7 +30,7 @@ signal enemy_dropped_loot(loot: Dictionary, world_pos: Vector3)
|
|||
@export var max_damage: int = 7
|
||||
@export var attack_range: float = 2.0
|
||||
@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 detection_range: float = 15.0
|
||||
@export var loot_table: LootTable = null
|
||||
|
|
@ -42,20 +42,44 @@ var target = null # Spieler
|
|||
# ZUSTAND
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
enum State { IDLE, CHASING, ATTACKING, DEAD }
|
||||
enum State { IDLE, PATROL, CHASING, ATTACKING, DEAD }
|
||||
var state: State = State.IDLE
|
||||
|
||||
var attack_cooldown: float = 0.0
|
||||
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
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 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
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
@onready var health_label: Label3D = $HealthDisplay/Label3D
|
||||
@onready var model: Node3D = $Model
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# READY
|
||||
|
|
@ -70,6 +94,98 @@ func _ready():
|
|||
nav_agent.path_desired_distance = 0.5
|
||||
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
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
|
@ -86,11 +202,24 @@ func _physics_process(delta):
|
|||
if attack_cooldown > 0:
|
||||
attack_cooldown -= delta
|
||||
|
||||
# Kein Ziel → Idle
|
||||
# Kein Ziel → Spieler suchen
|
||||
if target == null or not is_instance_valid(target):
|
||||
state = State.IDLE
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
target = null
|
||||
if state == State.CHASING or state == State.ATTACKING:
|
||||
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()
|
||||
return
|
||||
|
||||
|
|
@ -98,15 +227,22 @@ func _physics_process(delta):
|
|||
|
||||
match state:
|
||||
State.IDLE:
|
||||
_play_anim("idle")
|
||||
if distance <= detection_range:
|
||||
state = State.CHASING
|
||||
State.PATROL:
|
||||
if distance <= detection_range:
|
||||
state = State.CHASING
|
||||
else:
|
||||
_do_patrol(delta)
|
||||
State.CHASING:
|
||||
if distance <= attack_range:
|
||||
state = State.ATTACKING
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
else:
|
||||
_move_toward_target()
|
||||
_chase_target()
|
||||
_play_anim("run")
|
||||
State.ATTACKING:
|
||||
if distance > attack_range * 1.5:
|
||||
state = State.CHASING
|
||||
|
|
@ -116,22 +252,84 @@ func _physics_process(delta):
|
|||
_face_target()
|
||||
if attack_cooldown <= 0:
|
||||
_perform_attack()
|
||||
elif current_anim != "autoattack":
|
||||
_play_anim("idle")
|
||||
|
||||
move_and_slide()
|
||||
|
||||
func _move_toward_target():
|
||||
func _chase_target():
|
||||
if target == null:
|
||||
return
|
||||
nav_agent.target_position = target.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()
|
||||
var direction = (target.global_position - global_position)
|
||||
direction.y = 0
|
||||
if direction.length() < 0.1:
|
||||
return
|
||||
direction = direction.normalized()
|
||||
velocity.x = direction.x * move_speed
|
||||
velocity.z = direction.z * move_speed
|
||||
_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():
|
||||
if target == null:
|
||||
return
|
||||
|
|
@ -154,6 +352,7 @@ func _perform_attack():
|
|||
var damage = randi_range(min_damage, max_damage)
|
||||
target.take_damage(damage)
|
||||
attack_cooldown = attack_speed
|
||||
_play_anim("autoattack")
|
||||
print(name + " greift an: " + str(damage) + " Schaden")
|
||||
|
||||
func take_damage(amount: int):
|
||||
|
|
@ -161,6 +360,12 @@ func take_damage(amount: int):
|
|||
return
|
||||
current_hp = clamp(current_hp - amount, 0, max_hp)
|
||||
_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:
|
||||
_die()
|
||||
|
||||
|
|
@ -185,9 +390,12 @@ func _update_health_display():
|
|||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
func _die():
|
||||
if is_dead:
|
||||
return
|
||||
is_dead = true
|
||||
state = State.DEAD
|
||||
velocity = Vector3.ZERO
|
||||
_play_anim("death")
|
||||
print(name + " gestorben!")
|
||||
|
||||
# Loot generieren
|
||||
|
|
@ -195,11 +403,11 @@ func _die():
|
|||
var loot = loot_table.generate_loot()
|
||||
enemy_dropped_loot.emit(loot, global_position)
|
||||
|
||||
# XP und Respawn-Signal
|
||||
# XP und Respawn-Signal (nur einmal!)
|
||||
enemy_died.emit(global_position, xp_reward)
|
||||
|
||||
# Kollision deaktivieren und Node entfernen
|
||||
set_deferred("collision_layer", 0)
|
||||
set_deferred("collision_mask", 0)
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
queue_free()
|
||||
|
|
|
|||
28
enemy.tscn
28
enemy.tscn
|
|
@ -1,26 +1,28 @@
|
|||
[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"]
|
||||
radius = 0.4
|
||||
height = 1.8
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_7k104"]
|
||||
height = 2.208252
|
||||
|
||||
[node name="Enemy" type="CharacterBody3D"]
|
||||
[node name="Enemy" type="CharacterBody3D" unique_id=393882142]
|
||||
script = ExtResource("1_enemy")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
|
||||
shape = SubResource("CapsuleShape3D_1")
|
||||
[node name="Model" parent="." unique_id=842107644 instance=ExtResource("2_model")]
|
||||
|
||||
[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)
|
||||
|
||||
[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
|
||||
billboard = 3
|
||||
text = "50 / 50"
|
||||
font_size = 64
|
||||
font_size = 24
|
||||
outline_size = 8
|
||||
|
|
|
|||
25
player.gd
25
player.gd
|
|
@ -152,6 +152,7 @@ const ROLL_COOLDOWN: float = 1.5
|
|||
# wird nur aktualisiert wenn LMB NICHT gedrückt ist, damit
|
||||
# LMB-Kamerarotation die Laufrichtung nicht verändert
|
||||
var _movement_yaw: float = 0.0
|
||||
var _lmb_press_pos: Vector2 = Vector2.ZERO
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# NODE-REFERENZEN
|
||||
|
|
@ -173,6 +174,8 @@ var character_panel = null
|
|||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
func _ready():
|
||||
add_to_group("player")
|
||||
|
||||
# Jolt Physics: Boden sicher erkennen
|
||||
floor_snap_length = 0.3
|
||||
floor_max_angle = deg_to_rad(50.0)
|
||||
|
|
@ -696,6 +699,13 @@ func set_target(new_target, start_attack: bool = false):
|
|||
if start_attack:
|
||||
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):
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var viewport = get_viewport()
|
||||
|
|
@ -707,6 +717,9 @@ func _try_select_target(start_attack: bool = false):
|
|||
var result = space_state.intersect_ray(query)
|
||||
if result and result.collider.has_method("take_damage"):
|
||||
set_target(result.collider, start_attack)
|
||||
else:
|
||||
# Klick auf freie Fläche → Target entfernen
|
||||
clear_target()
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# ANIMATION SETUP
|
||||
|
|
@ -889,8 +902,8 @@ func _physics_process(delta):
|
|||
for key in consumable_cooldowns.keys():
|
||||
consumable_cooldowns[key] = max(0.0, consumable_cooldowns[key] - delta)
|
||||
|
||||
# ── Autoattack nach GCD ───────────────────────────────────
|
||||
if gcd_was_active and global_cooldown <= 0 and autoattack_active:
|
||||
# ── Autoattack nach GCD oder wenn in Range ───────────────
|
||||
if autoattack_active and global_cooldown <= 0 and not is_casting:
|
||||
perform_autoattack()
|
||||
|
||||
# ── Cast-System ───────────────────────────────────────────
|
||||
|
|
@ -921,9 +934,13 @@ func _physics_process(delta):
|
|||
if is_casting:
|
||||
_cancel_cast()
|
||||
|
||||
# ── Zielauswahl ───────────────────────────────────────────
|
||||
# ── Zielauswahl (nur Klick, nicht Drag) ───────────────────
|
||||
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"):
|
||||
_try_select_target(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||
radius = 0.4
|
||||
height = 1.8
|
||||
height = 2.088965
|
||||
|
||||
[node name="Player" type="CharacterBody3D" unique_id=1565111917]
|
||||
script = ExtResource("1_player")
|
||||
|
||||
[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")
|
||||
|
||||
[node name="Model" type="Node3D" parent="." unique_id=297754421]
|
||||
|
|
|
|||
15
world.gd
15
world.gd
|
|
@ -111,8 +111,9 @@ func _on_class_selected(character_class: CharacterClass):
|
|||
print("Klasse gewählt: ", character_class.class_name_de)
|
||||
|
||||
# Jetzt Gegner initialisieren
|
||||
var enemy = get_node("Enemy")
|
||||
_setup_enemy(enemy)
|
||||
for child in get_children():
|
||||
if child.has_method("take_damage") and child != player:
|
||||
_setup_enemy(child)
|
||||
|
||||
# Gegner initialisieren und Signal verbinden
|
||||
func _setup_enemy(enemy):
|
||||
|
|
@ -120,8 +121,10 @@ func _setup_enemy(enemy):
|
|||
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)
|
||||
if not enemy.enemy_died.is_connected(_on_enemy_died):
|
||||
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:
|
||||
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)
|
||||
|
||||
# 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...")
|
||||
await get_tree().create_timer(RESPAWN_TIME).timeout
|
||||
_spawn_enemy(spawn_position)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
[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="PackedScene" uid="uid://cvojaeanxugfj" path="res://enemy.tscn" id="3_enemy"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_fj7yv"]
|
||||
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")
|
||||
|
||||
[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]
|
||||
transform = Transform3D(-45, 0, 0, 0, -45, 0, 0, 0, -45, 0, 0, 0)
|
||||
|
||||
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=827244005]
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue