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 |
|
| 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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
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
|
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]
|
||||||
|
|
||||||
|
|
|
||||||
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
|
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.
Binary file not shown.
|
|
@ -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
238
enemy.gd
|
|
@ -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()
|
||||||
|
|
|
||||||
28
enemy.tscn
28
enemy.tscn
|
|
@ -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
|
||||||
|
|
|
||||||
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
|
# 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
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)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue