diff --git a/PROJEKTDOKU.md b/PROJEKTDOKU.md index d7edc14..2ddd905 100644 --- a/PROJEKTDOKU.md +++ b/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 ``` diff --git a/assets/Warrior+Animation/Walking Turn 180.fbx b/assets/Warrior+Animation/Walking Turn 180.fbx new file mode 100644 index 0000000..825478e Binary files /dev/null and b/assets/Warrior+Animation/Walking Turn 180.fbx differ diff --git a/assets/Warrior+Animation/Walking Turn 180.fbx.import b/assets/Warrior+Animation/Walking Turn 180.fbx.import new file mode 100644 index 0000000..3820213 --- /dev/null +++ b/assets/Warrior+Animation/Walking Turn 180.fbx.import @@ -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 diff --git a/assets/Warrior+Animation/idle.fbx b/assets/Warrior+Animation/idle.fbx deleted file mode 100644 index ccc21ff..0000000 Binary files a/assets/Warrior+Animation/idle.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/jump.fbx b/assets/Warrior+Animation/jump.fbx deleted file mode 100644 index cf721e5..0000000 Binary files a/assets/Warrior+Animation/jump.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/left strafe walking.fbx b/assets/Warrior+Animation/left strafe walking.fbx deleted file mode 100644 index 5645972..0000000 Binary files a/assets/Warrior+Animation/left strafe walking.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/left strafe.fbx b/assets/Warrior+Animation/left strafe.fbx deleted file mode 100644 index 881914f..0000000 Binary files a/assets/Warrior+Animation/left strafe.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/left turn 90.fbx b/assets/Warrior+Animation/left turn 90.fbx index 9d0a292..a393a70 100644 Binary files a/assets/Warrior+Animation/left turn 90.fbx and b/assets/Warrior+Animation/left turn 90.fbx differ diff --git a/assets/Warrior+Animation/left turn 90.fbx.import b/assets/Warrior+Animation/left turn 90.fbx.import index 12146e8..81529d9 100644 --- a/assets/Warrior+Animation/left turn 90.fbx.import +++ b/assets/Warrior+Animation/left turn 90.fbx.import @@ -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] diff --git a/assets/Warrior+Animation/left turn.fbx b/assets/Warrior+Animation/left turn.fbx deleted file mode 100644 index cee6230..0000000 Binary files a/assets/Warrior+Animation/left turn.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/right strafe walking.fbx b/assets/Warrior+Animation/right strafe walking.fbx deleted file mode 100644 index d56012f..0000000 Binary files a/assets/Warrior+Animation/right strafe walking.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/right strafe.fbx b/assets/Warrior+Animation/right strafe.fbx deleted file mode 100644 index c533196..0000000 Binary files a/assets/Warrior+Animation/right strafe.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/right turn 90.fbx b/assets/Warrior+Animation/right turn 90.fbx index 18fe6d4..51ef67e 100644 Binary files a/assets/Warrior+Animation/right turn 90.fbx and b/assets/Warrior+Animation/right turn 90.fbx differ diff --git a/assets/Warrior+Animation/right turn 90.fbx.import b/assets/Warrior+Animation/right turn 90.fbx.import index ef7b291..3a1b893 100644 --- a/assets/Warrior+Animation/right turn 90.fbx.import +++ b/assets/Warrior+Animation/right turn 90.fbx.import @@ -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] diff --git a/assets/Warrior+Animation/right turn.fbx b/assets/Warrior+Animation/right turn.fbx deleted file mode 100644 index 6c202f1..0000000 Binary files a/assets/Warrior+Animation/right turn.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/running.fbx b/assets/Warrior+Animation/running.fbx deleted file mode 100644 index 9c32abe..0000000 Binary files a/assets/Warrior+Animation/running.fbx and /dev/null differ diff --git a/assets/Warrior+Animation/walking.fbx b/assets/Warrior+Animation/walking.fbx deleted file mode 100644 index 501429c..0000000 Binary files a/assets/Warrior+Animation/walking.fbx and /dev/null differ diff --git a/camera_pivot.gd b/camera_pivot.gd index 3f8847d..442dfba 100644 --- a/camera_pivot.gd +++ b/camera_pivot.gd @@ -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: diff --git a/enemy.gd b/enemy.gd index 66d02fd..6a8239b 100644 --- a/enemy.gd +++ b/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() diff --git a/enemy.tscn b/enemy.tscn index 5c3c3b2..5fedf8d 100644 --- a/enemy.tscn +++ b/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 diff --git a/player.gd b/player.gd index 690d816..8a2f47b 100644 --- a/player.gd +++ b/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) diff --git a/player.tscn b/player.tscn index fd0a3d3..97587de 100644 --- a/player.tscn +++ b/player.tscn @@ -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] diff --git a/world.gd b/world.gd index aff6509..17369b4 100644 --- a/world.gd +++ b/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) diff --git a/world.tscn b/world.tscn index e7023d9..ce721c1 100644 --- a/world.tscn +++ b/world.tscn @@ -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)