diff --git a/PROJEKTDOKU.md b/PROJEKTDOKU.md index be6790a..73ddc3b 100644 --- a/PROJEKTDOKU.md +++ b/PROJEKTDOKU.md @@ -38,15 +38,28 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge ### world.tscn Hauptszene der Spielwelt. Zeigt bei Start das Hauptmenü (Einstellungen), dann Klassenauswahl. +Enthält Overworld (Gras-Terrain, Berg, Dungeon-Tor, Felsen, Bäume) und generiert Dungeon-Ebenen zur Laufzeit. ``` World (Node3D) ├── Player (player.tscn) -├── Enemy (enemy.tscn) -├── Boden (StaticBody3D) -│ ├── MeshInstance3D (Schachbrett-Shader) +├── Enemy (enemy.tscn) # Overworld-Gegner +├── Boden (StaticBody3D) # Gras-Terrain mit Noise-Shader +│ ├── MeshInstance3D │ └── CollisionShape3D ├── DirectionalLight3D -└── NavigationRegion3D +├── NavigationRegion3D # Overworld NavMesh (im Editor backen!) +├── Mountain (CSGCombiner3D) # Berg mit 4 CSGSphere3D, use_collision +├── DungeonGate # Tor zum Dungeon (Area3D + Label) +│ ├── GateArea (Area3D) +│ └── GateLabel (Label3D) +├── Rocks (9x CSGSphere3D) +├── Trees (6x CSGCylinder3D + CSGSphere3D) +└── [DungeonContainer] (zur Laufzeit) # Wird bei Dungeon-Eintritt generiert + ├── DungeonNavRegion (NavigationRegion3D) + ├── Floor/Wall/Ceiling Tiles (CSGBox3D) + ├── Enemies + ├── ReturnPortal (Area3D) + └── DeeperPortal (Area3D) ``` ### player.tscn @@ -63,12 +76,12 @@ Player (CharacterBody3D) UI-Panels (CharacterPanel, InventoryPanel, LootWindow, SkillPanel) werden zur Laufzeit erstellt. ### enemy.tscn -Ein Gegner mit Patrol-KI und Kampf-Animationen. +Ein Gegner (Goblin-Modell) mit Patrol-KI, NavMesh-Pathfinding und Kampf-Animationen. ``` Enemy (CharacterBody3D) -├── Model (castle_guard_01.fbx — Mixamo Charakter mit Skeleton3D + AnimationPlayer) -├── CollisionShape3D -├── NavigationAgent3D +├── Model (goblin_d_shareyko.fbx — Goblin mit Skeleton3D + AnimationPlayer) +├── CollisionShape3D (CapsuleShape3D, radius=0.35, height=1.4) +├── NavigationAgent3D (radius=0.4, für NavMesh-Pfadfindung) └── HealthDisplay (Node3D) └── Label3D ``` @@ -88,6 +101,12 @@ Enemy (CharacterBody3D) Lock-On springt automatisch zurück sobald LMB/RMB losgelassen wird und Ziel noch markiert ist. +### Kamera-Kollision +- Raycast vom Pivot zur gewünschten Kameraposition +- Bei Wandkollision: Kamera rückt näher an den Spieler (min 1.0 Abstand) +- Ignoriert CharacterBody3D (Gegner) — nur Wände/Terrain +- Smooth-Zoom zurück wenn Wand nicht mehr im Weg + --- ## Klassen, Stats & Level-System @@ -300,27 +319,35 @@ Stats werden in `_ready()` basierend auf `mob_level` skaliert (Basiswerte = Leve | 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 | +| DEAD | Spielt death-Animation, wird nach 10s 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 +- Patrol-Ziel wird auf nächsten begehbaren NavMesh-Punkt gesnapped - Bei Aggro sofort Wechsel zu run-Animation und move_speed -### Aggro +### Pathfinding (NavigationAgent3D) +- Nutzt `NavigationAgent3D` mit NavMesh-Pfadfindung (Dungeon) +- Gegner laufen um Wände und Ecken herum zum Spieler +- Pfad wird nur neu berechnet wenn Spieler sich > 1.5 Einheiten bewegt hat (Anti-Flicker) +- Fallback auf direkte Bewegung wenn kein NavMesh vorhanden (Overworld) + +### Aggro & Leash - **Detection Range:** Spieler wird automatisch erkannt wenn in 15m Reichweite - **Schaden-Aggro:** Bei Schaden sofort Wechsel zu CHASING, auch aus PATROL +- **Leash Range:** Gegner verliert Aggro wenn > 30 Einheiten vom Spawnpunkt entfernt - **Spieler-Suche:** Per `get_nodes_in_group("player")` ### Respawn -- Gegner spawnen nach 5 Sekunden am Ursprungsort neu +- Gegner spawnen nach 60 Sekunden am Ursprungsort neu - 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 +### Animationen (Goblin-Modell) +Werden zur Laufzeit aus FBX-Dateien geladen (assets/Goblin+Animation/): +- idle, walking, standard run, attack, die, left turn 90, right turn 90 - Root Motion wird automatisch entfernt (`_strip_root_motion`) ### Loot-Drops @@ -342,7 +369,7 @@ Charaktermodelle stammen von Mixamo (castle_guard_01.fbx) und werden mit separat 4. Loop-Modus wird gesetzt (Bewegungsanimationen loopen, Angriff/Tod nicht) 5. Animation wird der AnimationLibrary hinzugefügt -### Verfügbare Animationen (assets/Warrior+Animation/) +### Spieler-Animationen (assets/Warrior+Animation/) | Animation | Datei | Loop | Verwendung | |---|---|---|---| | idle | idle.fbx | Ja | Stillstehen | @@ -361,8 +388,19 @@ Charaktermodelle stammen von Mixamo (castle_guard_01.fbx) und werden mit separat | 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) | +| turn_left | Left Turn 90.fbx | Nein | 90° links | +| turn_right | Right Turn 90.fbx | Nein | 90° rechts | + +### Gegner-Animationen (assets/Goblin+Animation/) +| Animation | Datei | Loop | Verwendung | +|---|---|---|---| +| idle | idle.fbx | Ja | Stillstehen | +| walk | walking.fbx | Ja | Patrol-Laufen | +| run | standard run.fbx | Ja | Rennen (Aggro) | +| autoattack | attack.fbx | Nein | Angriff | +| death | die.fbx | Nein | Tod | +| turn_left | left turn 90.fbx | Nein | 90° links (Patrol) | +| turn_right | right turn 90.fbx | Nein | 90° rechts (Patrol) | --- @@ -450,13 +488,53 @@ Rarity steigt durch Skill-Generationen (Verbindung/Fusion) — nicht durch Grind --- +## Dungeon-System (world.gd) + +### Überblick +Prozedural generierte Dungeon-Ebenen innerhalb derselben Szene (kein Szenenwechsel). Overworld wird versteckt, Dungeon als Kind-Nodes generiert. Spieler-State (HP, Inventar, Level) bleibt erhalten. + +### Dungeon-Generierung +- **Grid-basiert:** 60x60 Zellen, TILE_SIZE = 3.0 +- **Räume:** 6-10 Räume, Größe 4-9 Zellen +- **Gänge:** L-förmige Korridore zwischen Räumen (Breite 2) +- **Geometrie:** CSGBox3D für Boden, Wände (Höhe 6.0), Decke, jeweils mit `use_collision` +- **Beleuchtung:** OmniLight3D pro Raum + Decken-SpotLights in Gängen +- **Environment:** Eigene WorldEnvironment mit Fog, kein Himmel + +### NavMesh (Navigation) +- Pro Dungeon-Ebene wird ein `NavigationRegion3D` mit manuell gebautem `NavigationMesh` erstellt +- Positions-basierte Vertex-Deduplizierung: gleiche Position → gleicher Index → zusammenhängendes NavMesh +- Wand-Margin (0.5): Kanten an Wänden werden nach innen versetzt, damit Gegner Abstand halten +- Edge-Connection-Margin auf 0.6 gesetzt + +### Portal-System +| Portal | Farbe | Funktion | +|---|---|---| +| Rückkehr-Portal | Blau | Im ersten Raum, führt eine Ebene hoch oder raus | +| Deeper-Portal | Rot/Orange | Im letzten Raum, führt eine Ebene tiefer | + +- **Ebene 1:** Rückkehr-Portal → direkt zur Overworld +- **Ebene 2+:** Rückkehr-Portal → Auswahl: "Eine Ebene hoch" oder "Dungeon verlassen" +- **Interaktion:** E-Taste, Labels zeigen Ebenen-Info + +### Dungeon-Persistenz +- Generierte Ebenen werden in `saved_dungeons` Dictionary gespeichert (Level → {grid, rooms}) +- Beim Zurückgehen wird das gespeicherte Layout wiederhergestellt +- Beim Verlassen des Dungeons werden alle gespeicherten Ebenen gelöscht + +### Gegner im Dungeon +- Spawnen in Räumen 2 bis N-1 (nicht im Start- und End-Raum) +- `mob_level = dungeon_level` (steigt mit jeder Ebene) +- Nutzen NavMesh-Pathfinding um Wände herum + +--- + ## Geplante Features - [ ] Skill Mastery & Fusion System (siehe Konzept oben) - [ ] Wut-Ressource für Krieger - [ ] Spell-System (Feuerbälle etc.) - [ ] Schadenstypen (Physical, Fire, Ice, Lightning, Poison) - [ ] Mehrere Gegnertypen -- [ ] Dungeon-Level mit Wänden und Räumen - [ ] Multiplayer (bis zu 6 Spieler) - [ ] Boss-Gegner - [ ] Item-Shop / Händler @@ -467,7 +545,8 @@ Rarity steigt durch Skill-Generationen (Verbindung/Fusion) — nicht durch Grind ``` DungeonCrawler/ ├── assets/ -│ ├── Warrior+Animation/ # Mixamo Charakter + Animationen (castle_guard_01.fbx + FBX) +│ ├── Warrior+Animation/ # Spieler-Charakter + Animationen (castle_guard_01.fbx + FBX) +│ ├── Goblin+Animation/ # Gegner-Modell + Animationen (goblin_d_shareyko.fbx + FBX) │ └── kenney_animated-characters-1/ # Kenney Animated Characters Pack ├── classes/ # Klassen-Definitionen (.tres) │ ├── warrior.tres diff --git a/assets/Goblin+Animation/attack.fbx b/assets/Goblin+Animation/attack.fbx new file mode 100644 index 0000000..ea8b803 Binary files /dev/null and b/assets/Goblin+Animation/attack.fbx differ diff --git a/assets/Goblin+Animation/attack.fbx.import b/assets/Goblin+Animation/attack.fbx.import new file mode 100644 index 0000000..82bc5c5 --- /dev/null +++ b/assets/Goblin+Animation/attack.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://1nh16lj56b6v" +path="res://.godot/imported/attack.fbx-74f24f5aa3abe1e08504a4c0a89ebd90.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/attack.fbx" +dest_files=["res://.godot/imported/attack.fbx-74f24f5aa3abe1e08504a4c0a89ebd90.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/Goblin+Animation/die.fbx b/assets/Goblin+Animation/die.fbx new file mode 100644 index 0000000..5401320 Binary files /dev/null and b/assets/Goblin+Animation/die.fbx differ diff --git a/assets/Goblin+Animation/die.fbx.import b/assets/Goblin+Animation/die.fbx.import new file mode 100644 index 0000000..460b6f1 --- /dev/null +++ b/assets/Goblin+Animation/die.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b7opwpnodeke4" +path="res://.godot/imported/die.fbx-9b1eb5481e87e52c2e23336481a99db2.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/die.fbx" +dest_files=["res://.godot/imported/die.fbx-9b1eb5481e87e52c2e23336481a99db2.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/Goblin+Animation/goblin_d_shareyko.fbx b/assets/Goblin+Animation/goblin_d_shareyko.fbx new file mode 100644 index 0000000..c7bacd4 Binary files /dev/null and b/assets/Goblin+Animation/goblin_d_shareyko.fbx differ diff --git a/assets/Goblin+Animation/goblin_d_shareyko.fbx.import b/assets/Goblin+Animation/goblin_d_shareyko.fbx.import new file mode 100644 index 0000000..6dd264f --- /dev/null +++ b/assets/Goblin+Animation/goblin_d_shareyko.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bpmvu5c2c0nty" +path="res://.godot/imported/goblin_d_shareyko.fbx-19a0d07a917db1228319e2d9380f00d1.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/goblin_d_shareyko.fbx" +dest_files=["res://.godot/imported/goblin_d_shareyko.fbx-19a0d07a917db1228319e2d9380f00d1.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/Goblin+Animation/goblin_d_shareyko_0.png b/assets/Goblin+Animation/goblin_d_shareyko_0.png new file mode 100644 index 0000000..45a85fb Binary files /dev/null and b/assets/Goblin+Animation/goblin_d_shareyko_0.png differ diff --git a/assets/Goblin+Animation/goblin_d_shareyko_0.png.import b/assets/Goblin+Animation/goblin_d_shareyko_0.png.import new file mode 100644 index 0000000..9bd585b --- /dev/null +++ b/assets/Goblin+Animation/goblin_d_shareyko_0.png.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvy7pen32ajri" +path.s3tc="res://.godot/imported/goblin_d_shareyko_0.png-6f7e60f7d1dfca06b1589009655e64ba.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} +generator_parameters={ +"md5": "8e5ff3ab5d9e47e8fcfa88b364ca7a3e" +} + +[deps] + +source_file="res://assets/Goblin+Animation/goblin_d_shareyko_0.png" +dest_files=["res://.godot/imported/goblin_d_shareyko_0.png-6f7e60f7d1dfca06b1589009655e64ba.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets/Goblin+Animation/goblin_d_shareyko_1.png b/assets/Goblin+Animation/goblin_d_shareyko_1.png new file mode 100644 index 0000000..f77f788 Binary files /dev/null and b/assets/Goblin+Animation/goblin_d_shareyko_1.png differ diff --git a/assets/Goblin+Animation/goblin_d_shareyko_1.png.import b/assets/Goblin+Animation/goblin_d_shareyko_1.png.import new file mode 100644 index 0000000..2592aa9 --- /dev/null +++ b/assets/Goblin+Animation/goblin_d_shareyko_1.png.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4fwlvu5lvb76" +path.s3tc="res://.godot/imported/goblin_d_shareyko_1.png-e66469a7a38b139ca8bd4daaee8d6fd3.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} +generator_parameters={ +"md5": "9cfcb6de764cdf7fff1d5c61a40607c7" +} + +[deps] + +source_file="res://assets/Goblin+Animation/goblin_d_shareyko_1.png" +dest_files=["res://.godot/imported/goblin_d_shareyko_1.png-e66469a7a38b139ca8bd4daaee8d6fd3.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=1 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=1 +roughness/src_normal="res://assets/Goblin+Annimation/goblin_d_shareyko_1.png" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets/Goblin+Animation/goblin_d_shareyko_2.png b/assets/Goblin+Animation/goblin_d_shareyko_2.png new file mode 100644 index 0000000..80b5e65 Binary files /dev/null and b/assets/Goblin+Animation/goblin_d_shareyko_2.png differ diff --git a/assets/Goblin+Animation/goblin_d_shareyko_2.png.import b/assets/Goblin+Animation/goblin_d_shareyko_2.png.import new file mode 100644 index 0000000..455e50c --- /dev/null +++ b/assets/Goblin+Animation/goblin_d_shareyko_2.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dlytifou244vw" +path="res://.godot/imported/goblin_d_shareyko_2.png-fa1609abb3cfae8642f95c6d3f7a9e0b.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "3b587dba802d634d0c459461e6207307" +} + +[deps] + +source_file="res://assets/Goblin+Animation/goblin_d_shareyko_2.png" +dest_files=["res://.godot/imported/goblin_d_shareyko_2.png-fa1609abb3cfae8642f95c6d3f7a9e0b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/Goblin+Animation/idle.fbx b/assets/Goblin+Animation/idle.fbx new file mode 100644 index 0000000..b36d5bc Binary files /dev/null and b/assets/Goblin+Animation/idle.fbx differ diff --git a/assets/Goblin+Animation/idle.fbx.import b/assets/Goblin+Animation/idle.fbx.import new file mode 100644 index 0000000..6a15eca --- /dev/null +++ b/assets/Goblin+Animation/idle.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://vkx02tvlsdh" +path="res://.godot/imported/idle.fbx-10b8e348b24126ecb7911637de5ed41b.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/idle.fbx" +dest_files=["res://.godot/imported/idle.fbx-10b8e348b24126ecb7911637de5ed41b.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/Goblin+Animation/jump.fbx b/assets/Goblin+Animation/jump.fbx new file mode 100644 index 0000000..3005ab9 Binary files /dev/null and b/assets/Goblin+Animation/jump.fbx differ diff --git a/assets/Goblin+Animation/jump.fbx.import b/assets/Goblin+Animation/jump.fbx.import new file mode 100644 index 0000000..6aff03a --- /dev/null +++ b/assets/Goblin+Animation/jump.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://btlqv3sdusu3" +path="res://.godot/imported/jump.fbx-c935f1d10578ccbe3975acc112927dfc.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/jump.fbx" +dest_files=["res://.godot/imported/jump.fbx-c935f1d10578ccbe3975acc112927dfc.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/Goblin+Animation/left strafe walking.fbx b/assets/Goblin+Animation/left strafe walking.fbx new file mode 100644 index 0000000..d3815be Binary files /dev/null and b/assets/Goblin+Animation/left strafe walking.fbx differ diff --git a/assets/Goblin+Animation/left strafe walking.fbx.import b/assets/Goblin+Animation/left strafe walking.fbx.import new file mode 100644 index 0000000..5ae96cd --- /dev/null +++ b/assets/Goblin+Animation/left strafe walking.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://c5btq8xjlu871" +path="res://.godot/imported/left strafe walking.fbx-91dd542134cbe48608f36538bb0cc694.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/left strafe walking.fbx" +dest_files=["res://.godot/imported/left strafe walking.fbx-91dd542134cbe48608f36538bb0cc694.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/Goblin+Animation/left strafe.fbx b/assets/Goblin+Animation/left strafe.fbx new file mode 100644 index 0000000..eb359b9 Binary files /dev/null and b/assets/Goblin+Animation/left strafe.fbx differ diff --git a/assets/Goblin+Animation/left strafe.fbx.import b/assets/Goblin+Animation/left strafe.fbx.import new file mode 100644 index 0000000..2df8b43 --- /dev/null +++ b/assets/Goblin+Animation/left strafe.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cic7ndddknq5d" +path="res://.godot/imported/left strafe.fbx-cb42c3e72794269447ef40352a1eda03.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/left strafe.fbx" +dest_files=["res://.godot/imported/left strafe.fbx-cb42c3e72794269447ef40352a1eda03.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/Goblin+Animation/left turn 90.fbx b/assets/Goblin+Animation/left turn 90.fbx new file mode 100644 index 0000000..1065722 Binary files /dev/null and b/assets/Goblin+Animation/left turn 90.fbx differ diff --git a/assets/Goblin+Animation/left turn 90.fbx.import b/assets/Goblin+Animation/left turn 90.fbx.import new file mode 100644 index 0000000..7e251a4 --- /dev/null +++ b/assets/Goblin+Animation/left turn 90.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://d1assifgeqf6q" +path="res://.godot/imported/left turn 90.fbx-f708d959e637559ca294a793eaf09bda.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/left turn 90.fbx" +dest_files=["res://.godot/imported/left turn 90.fbx-f708d959e637559ca294a793eaf09bda.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/Goblin+Animation/right strafe walking.fbx b/assets/Goblin+Animation/right strafe walking.fbx new file mode 100644 index 0000000..c80374e Binary files /dev/null and b/assets/Goblin+Animation/right strafe walking.fbx differ diff --git a/assets/Goblin+Animation/right strafe walking.fbx.import b/assets/Goblin+Animation/right strafe walking.fbx.import new file mode 100644 index 0000000..dab97cd --- /dev/null +++ b/assets/Goblin+Animation/right strafe walking.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cwb5bwv8tv38m" +path="res://.godot/imported/right strafe walking.fbx-aa95bdbda650ae471db0052435fc180b.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/right strafe walking.fbx" +dest_files=["res://.godot/imported/right strafe walking.fbx-aa95bdbda650ae471db0052435fc180b.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/Goblin+Animation/right strafe.fbx b/assets/Goblin+Animation/right strafe.fbx new file mode 100644 index 0000000..7dfe74d Binary files /dev/null and b/assets/Goblin+Animation/right strafe.fbx differ diff --git a/assets/Goblin+Animation/right strafe.fbx.import b/assets/Goblin+Animation/right strafe.fbx.import new file mode 100644 index 0000000..3698a48 --- /dev/null +++ b/assets/Goblin+Animation/right strafe.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://pfcvjhiyap1j" +path="res://.godot/imported/right strafe.fbx-4e62ff6b8b107948b9183770097152fe.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/right strafe.fbx" +dest_files=["res://.godot/imported/right strafe.fbx-4e62ff6b8b107948b9183770097152fe.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/Goblin+Animation/right turn 90.fbx b/assets/Goblin+Animation/right turn 90.fbx new file mode 100644 index 0000000..e52ebc3 Binary files /dev/null and b/assets/Goblin+Animation/right turn 90.fbx differ diff --git a/assets/Goblin+Animation/right turn 90.fbx.import b/assets/Goblin+Animation/right turn 90.fbx.import new file mode 100644 index 0000000..82bd939 --- /dev/null +++ b/assets/Goblin+Animation/right turn 90.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://p23ow3gyhmjk" +path="res://.godot/imported/right turn 90.fbx-37203ca5c7b5d817bc21c0c0e296e833.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/right turn 90.fbx" +dest_files=["res://.godot/imported/right turn 90.fbx-37203ca5c7b5d817bc21c0c0e296e833.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/Goblin+Animation/standard run.fbx b/assets/Goblin+Animation/standard run.fbx new file mode 100644 index 0000000..f1f0ea8 Binary files /dev/null and b/assets/Goblin+Animation/standard run.fbx differ diff --git a/assets/Goblin+Animation/standard run.fbx.import b/assets/Goblin+Animation/standard run.fbx.import new file mode 100644 index 0000000..e89978a --- /dev/null +++ b/assets/Goblin+Animation/standard run.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://fbyvl4ux7pxl" +path="res://.godot/imported/standard run.fbx-dc8227b4d9d45456cda6b01eff9a394f.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/standard run.fbx" +dest_files=["res://.godot/imported/standard run.fbx-dc8227b4d9d45456cda6b01eff9a394f.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/Goblin+Animation/walking.fbx b/assets/Goblin+Animation/walking.fbx new file mode 100644 index 0000000..a2a5621 Binary files /dev/null and b/assets/Goblin+Animation/walking.fbx differ diff --git a/assets/Goblin+Animation/walking.fbx.import b/assets/Goblin+Animation/walking.fbx.import new file mode 100644 index 0000000..97bd9a2 --- /dev/null +++ b/assets/Goblin+Animation/walking.fbx.import @@ -0,0 +1,44 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cb05qpxan1tw6" +path="res://.godot/imported/walking.fbx-99408d8bc40ff5ad02c3ded8ae6dba68.scn" + +[deps] + +source_file="res://assets/Goblin+Animation/walking.fbx" +dest_files=["res://.godot/imported/walking.fbx-99408d8bc40ff5ad02c3ded8ae6dba68.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/camera_pivot.gd b/camera_pivot.gd index 442dfba..8e1cf09 100644 --- a/camera_pivot.gd +++ b/camera_pivot.gd @@ -27,12 +27,14 @@ extends Node3D var pitch: float = 0.0 var world_yaw: float = 0.0 # Absolute Weltausrichtung der Kamera (unabhängig von Spielerrotation) +var desired_zoom: float = 10.0 # Gewünschter Zoom-Abstand @onready var camera = $Camera3D func _ready(): Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) world_yaw = get_parent().rotation.y + desired_zoom = camera.position.z func _input(event): var player = get_parent() @@ -56,9 +58,9 @@ func _input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: - camera.position.z = clamp(camera.position.z + zoom_speed, min_zoom, max_zoom) + desired_zoom = clamp(desired_zoom + zoom_speed, min_zoom, max_zoom) if event.button_index == MOUSE_BUTTON_WHEEL_UP: - camera.position.z = clamp(camera.position.z - zoom_speed, min_zoom, max_zoom) + desired_zoom = clamp(desired_zoom - zoom_speed, min_zoom, max_zoom) func _process(delta): var player = get_parent() @@ -77,3 +79,23 @@ func _process(delta): # Lokale Rotation so setzen dass Kamera immer auf world_yaw zeigt rotation.y = world_yaw - player.rotation.y + + # Kamera-Kollision: Raycast vom Pivot zur gewünschten Kameraposition + var space_state = get_world_3d().direct_space_state + var pivot_pos = global_position + # Kamera sitzt auf lokaler Z-Achse des Pivots + var desired_cam_local = Vector3(0, 0, desired_zoom) + var desired_cam_pos = global_transform * desired_cam_local + + var query = PhysicsRayQueryParameters3D.create(pivot_pos, desired_cam_pos) + query.exclude = [player.get_rid()] + query.collide_with_bodies = true + query.collide_with_areas = false + query.collision_mask = 1 + var result = space_state.intersect_ray(query) + + if result and not result.collider is CharacterBody3D: + var hit_dist = pivot_pos.distance_to(result.position) - 0.5 + camera.position.z = clamp(hit_dist, 1.0, desired_zoom) + else: + camera.position.z = lerp(camera.position.z, desired_zoom, delta * 5.0) diff --git a/enemy.gd b/enemy.gd index 2992524..c057323 100644 --- a/enemy.gd +++ b/enemy.gd @@ -34,6 +34,7 @@ signal enemy_dropped_loot(loot: Dictionary, world_pos: Vector3) @export var xp_reward: int = 20 @export var mob_level: int = 1 @export var detection_range: float = 15.0 +@export var leash_range: float = 30.0 # Max Entfernung vom Spawn bevor Aggro verloren geht @export var loot_table: LootTable = null var current_hp: int @@ -62,13 +63,13 @@ 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" +const ANIM_IDLE = "res://assets/Goblin+Animation/idle.fbx" +const ANIM_WALK = "res://assets/Goblin+Animation/walking.fbx" +const ANIM_RUN = "res://assets/Goblin+Animation/standard run.fbx" +const ANIM_AUTOATTACK = "res://assets/Goblin+Animation/attack.fbx" +const ANIM_DEATH = "res://assets/Goblin+Animation/die.fbx" +const ANIM_TURN_LEFT = "res://assets/Goblin+Animation/left turn 90.fbx" +const ANIM_TURN_RIGHT = "res://assets/Goblin+Animation/right turn 90.fbx" var anim_player: AnimationPlayer = null var current_anim: String = "" @@ -108,6 +109,7 @@ func _ready(): # NavigationAgent konfigurieren nav_agent.path_desired_distance = 0.5 nav_agent.target_desired_distance = attack_range * 0.9 + nav_agent.radius = 0.4 # Gegner-Breite für Pfadberechnung # Animationen einrichten _setup_animations() @@ -240,6 +242,16 @@ func _physics_process(delta): var distance = global_position.distance_to(target.global_position) + # Leash-Check: Zu weit vom Spawn → Aggro verlieren, zurücklaufen + var dist_from_spawn = global_position.distance_to(spawn_position) + if dist_from_spawn > leash_range and (state == State.CHASING or state == State.ATTACKING): + target = null + state = State.PATROL + _pick_patrol_target() + _do_patrol(delta) + move_and_slide() + return + match state: State.IDLE: _play_anim("idle") @@ -272,17 +284,39 @@ func _physics_process(delta): move_and_slide() +func _has_navmesh() -> bool: + var map_rid = get_world_3d().navigation_map + return NavigationServer3D.map_get_regions(map_rid).size() > 0 + +func _move_toward(goal: Vector3, speed: float): + # Nur neuen Pfad berechnen wenn sich das Ziel deutlich bewegt hat + if nav_agent.target_position.distance_to(goal) > 1.5: + nav_agent.target_position = goal + if nav_agent.is_navigation_finished(): + return + var path = nav_agent.get_current_navigation_path() + var next_pos = nav_agent.get_next_path_position() + var direction = (next_pos - global_position) + direction.y = 0 + if direction.length() < 0.1: + if not _has_navmesh() or path.size() <= 1: + # Kein NavMesh oder kein gültiger Pfad → direkte Bewegung + direction = (goal - global_position) + direction.y = 0 + if direction.length() < 0.1: + return + direction = direction.normalized() + velocity.x = direction.x * speed + velocity.z = direction.z * speed + _face_direction(direction) + + func _chase_target(): if target == null: return - var direction = (target.global_position - global_position) - direction.y = 0 - if direction.length() < 0.1: + if global_position.distance_to(target.global_position) < 0.5: return - direction = direction.normalized() - velocity.x = direction.x * move_speed - velocity.z = direction.z * move_speed - _face_direction(direction) + _move_toward(target.global_position, move_speed) func _do_patrol(delta: float): # Turn-Animation läuft → warten @@ -302,9 +336,8 @@ func _do_patrol(delta: float): _turn_toward_patrol_target() return - # Zum Patrol-Ziel laufen - var dist = global_position.distance_to(patrol_target) - if dist <= 1.0: + # Zum Patrol-Ziel laufen (via NavMesh mit Fallback) + if global_position.distance_to(patrol_target) <= 1.0: # Ziel erreicht → kurz warten, neues Ziel velocity.x = 0 velocity.z = 0 @@ -313,12 +346,7 @@ func _do_patrol(delta: float): _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) + _move_toward(patrol_target, patrol_speed) _play_anim("walk") func _turn_toward_patrol_target(): @@ -343,7 +371,15 @@ func _turn_toward_patrol_target(): 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) + var raw_target = spawn_position + Vector3(cos(angle) * dist, 0, sin(angle) * dist) + # Auf nächsten begehbaren Punkt snappen (verhindert Ziele in Wänden) + var map_rid = get_world_3d().navigation_map + var snapped = NavigationServer3D.map_get_closest_point(map_rid, raw_target) + # Fallback: wenn Snap fehlschlägt (kein NavMesh), Rohziel verwenden + if snapped == Vector3.ZERO and raw_target != Vector3.ZERO: + patrol_target = raw_target + else: + patrol_target = snapped func _face_target(): if target == null: @@ -424,5 +460,5 @@ func _die(): # Kollision deaktivieren und Node entfernen set_deferred("collision_layer", 0) set_deferred("collision_mask", 0) - await get_tree().create_timer(2.0).timeout + await get_tree().create_timer(10.0).timeout queue_free() diff --git a/enemy.tscn b/enemy.tscn index 5fedf8d..120ad31 100644 --- a/enemy.tscn +++ b/enemy.tscn @@ -1,10 +1,11 @@ [gd_scene format=3 uid="uid://cvojaeanxugfj"] [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"] +[ext_resource type="PackedScene" path="res://assets/Goblin+Animation/goblin_d_shareyko.fbx" id="2_model"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_7k104"] -height = 2.208252 +radius = 0.35 +height = 1.4 [node name="Enemy" type="CharacterBody3D" unique_id=393882142] script = ExtResource("1_enemy") @@ -12,13 +13,13 @@ script = ExtResource("1_enemy") [node name="Model" parent="." unique_id=842107644 instance=ExtResource("2_model")] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1531674298] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.010422, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7, 0) shape = SubResource("CapsuleShape3D_7k104") [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, 1.6, 0) [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) diff --git a/loot_tables/goblin_loot.tres b/loot_tables/goblin_loot.tres index 5ce2e7d..c3b1fde 100644 --- a/loot_tables/goblin_loot.tres +++ b/loot_tables/goblin_loot.tres @@ -31,7 +31,6 @@ drop_chance = 0.15 [sub_resource type="Resource" id="entry_5"] script = ExtResource("2") item = ExtResource("7") -drop_chance = 0.1 [resource] script = ExtResource("1") diff --git a/project.godot b/project.godot index 3a0d0a5..432bd34 100644 --- a/project.godot +++ b/project.godot @@ -122,6 +122,11 @@ walk_toggle={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194330,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +interact={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} [physics] diff --git a/world.gd b/world.gd index 25af806..0d3d526 100644 --- a/world.gd +++ b/world.gd @@ -3,18 +3,19 @@ # Spielwelt-Controller # # Verantwortlichkeiten: -# • Prozeduraler Boden-Shader (Schachbrettmuster, world-space stabil) -# • Prozeduraler Himmel (ProceduralSkyMaterial + WorldEnvironment) +# • Overworld + Dungeon in einer Szene (kein Szenenwechsel) +# • Prozeduraler Boden-Shader, Himmel # • Hauptmenü → Klassenauswahl → Spielinitialisierung # • Startausrüstung je nach Klasse # • Gegner-Setup, Loot-Weiterleitung, Respawn nach Tod +# • Dungeon-Generierung und Portal-System # ───────────────────────────────────────────────────────────────────────────── extends Node3D const ENEMY_SCENE = preload("res://enemy.tscn") const MAIN_MENU = preload("res://main_menu.tscn") const CLASS_SELECTION_MENU = preload("res://class_selection_menu.tscn") -const RESPAWN_TIME = 5.0 +const RESPAWN_TIME = 60.0 # Startausrüstung const STARTER_SWORD = preload("res://equipment/iron_sword.tres") @@ -25,8 +26,41 @@ const STARTER_POTION = preload("res://consumables/small_hp_potion.tres") # Loot Tables const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres") +# Dungeon-Konstanten +const TILE_SIZE = 3.0 +const WALL_HEIGHT = 6.0 +const GRID_WIDTH = 60 +const GRID_HEIGHT = 60 +const MIN_ROOMS = 6 +const MAX_ROOMS = 10 +const MIN_ROOM_SIZE = 4 +const MAX_ROOM_SIZE = 9 + @onready var player = $Player @onready var floor_mesh = $Boden/MeshInstance3D +@onready var gate_area = $DungeonGate/GateArea +@onready var gate_label = $DungeonGate/GateLabel + +var in_dungeon: bool = false +var dungeon_level: int = 0 # 0 = Overworld, 1+ = Dungeon-Ebene +var overworld_nodes: Array = [] # Nodes die im Dungeon versteckt werden +var dungeon_container: Node3D = null +var dungeon_env: WorldEnvironment = null +var overworld_env: WorldEnvironment = null +var return_portal_area: Area3D = null +var return_portal_label: Label3D = null +var deeper_portal_area: Area3D = null +var deeper_portal_label: Label3D = null +var player_overworld_pos: Vector3 = Vector3.ZERO +var portal_choice_panel: PanelContainer = null +var overworld_env_resource: Environment = null + +# Dungeon-Daten +var dungeon_grid: Array = [] +var dungeon_rooms: Array = [] + +# Gespeicherte Dungeon-Ebenen (Level → {grid, rooms}) +var saved_dungeons: Dictionary = {} func _ready(): _setup_floor_material() @@ -55,37 +89,667 @@ func _setup_sky(): env.ambient_light_energy = 0.6 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC - var world_env = WorldEnvironment.new() - world_env.environment = env - add_child(world_env) + overworld_env = WorldEnvironment.new() + overworld_env.name = "OverworldEnv" + overworld_env.environment = env + add_child(overworld_env) func _setup_floor_material(): var shader = Shader.new() shader.code = """ shader_type spatial; -uniform float grid_size : hint_range(0.5, 10.0) = 2.0; -uniform vec4 color_a : source_color = vec4(0.22, 0.22, 0.22, 1.0); -uniform vec4 color_b : source_color = vec4(0.38, 0.38, 0.38, 1.0); +uniform vec4 grass_color_a : source_color = vec4(0.18, 0.42, 0.12, 1.0); +uniform vec4 grass_color_b : source_color = vec4(0.22, 0.50, 0.15, 1.0); +uniform vec4 dirt_color : source_color = vec4(0.35, 0.25, 0.15, 1.0); +uniform float noise_scale : hint_range(0.01, 0.5) = 0.08; +uniform float dirt_threshold : hint_range(0.0, 1.0) = 0.72; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} void fragment() { vec3 world_pos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; - vec2 cell = floor(world_pos.xz / grid_size); - float checker = mod(cell.x + cell.y, 2.0); - ALBEDO = mix(color_a.rgb, color_b.rgb, checker); - ROUGHNESS = 0.85; + vec2 uv = world_pos.xz * noise_scale; + float n1 = noise(uv * 3.0); + float n2 = noise(uv * 7.0 + vec2(50.0)); + float n3 = noise(uv * 15.0 + vec2(100.0)); + float combined = n1 * 0.5 + n2 * 0.35 + n3 * 0.15; + vec3 grass = mix(grass_color_a.rgb, grass_color_b.rgb, n2); + vec3 col = mix(grass, dirt_color.rgb, step(dirt_threshold, combined)); + ALBEDO = col; + ROUGHNESS = 0.92; METALLIC = 0.0; + NORMAL_MAP = vec3(n2 * 0.3, n3 * 0.3, 1.0); } """ var mat = ShaderMaterial.new() mat.shader = shader floor_mesh.material_override = mat +# ───────────────────────────────────────────────────────────────────────────── +# PROCESS +# ───────────────────────────────────────────────────────────────────────────── + +func _process(_delta): + if in_dungeon: + _check_return_portal() + _check_deeper_portal() + else: + _check_gate_proximity() + +func _check_gate_proximity(): + if not player or not gate_area or not gate_label: + return + var dist = player.global_position.distance_to(gate_area.global_position) + gate_label.visible = dist < 6.0 + if dist < 6.0 and Input.is_action_just_pressed("interact"): + _enter_dungeon() + +func _check_return_portal(): + if not player or not return_portal_area or not return_portal_label: + return + if portal_choice_panel and is_instance_valid(portal_choice_panel): + return # Menü ist offen + var dist = player.global_position.distance_to(return_portal_area.global_position) + return_portal_label.visible = dist < 6.0 + if dist < 6.0 and Input.is_action_just_pressed("interact"): + if dungeon_level <= 1: + _exit_dungeon() + else: + _show_portal_choice() + +func _check_deeper_portal(): + if not player or not deeper_portal_area or not deeper_portal_label: + return + var dist = player.global_position.distance_to(deeper_portal_area.global_position) + deeper_portal_label.visible = dist < 6.0 + if dist < 6.0 and Input.is_action_just_pressed("interact"): + _go_deeper() + +# ───────────────────────────────────────────────────────────────────────────── +# DUNGEON BETRETEN / VERLASSEN +# ───────────────────────────────────────────────────────────────────────────── + +func _enter_dungeon(): + in_dungeon = true + dungeon_level = 1 + player_overworld_pos = player.global_position + + # Overworld-Nodes verstecken + _hide_overworld() + + # Ersten Dungeon generieren + _generate_dungeon_level() + +func _go_deeper(): + # Aktuelle Ebene speichern + _save_current_dungeon() + _clear_dungeon() + dungeon_level += 1 + _generate_dungeon_level() + +func _go_up(): + # Aktuelle Ebene speichern + _save_current_dungeon() + _clear_dungeon() + dungeon_level -= 1 + _generate_dungeon_level() + +func _save_current_dungeon(): + saved_dungeons[dungeon_level] = { + "grid": dungeon_grid.duplicate(true), + "rooms": dungeon_rooms.duplicate(true) + } + +func _show_portal_choice(): + get_tree().paused = true + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + + portal_choice_panel = PanelContainer.new() + portal_choice_panel.process_mode = Node.PROCESS_MODE_ALWAYS + + var vbox = VBoxContainer.new() + vbox.add_theme_constant_override("separation", 10) + + var title = Label.new() + title.text = "Wohin möchtest du gehen?" + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox.add_child(title) + + var btn_up = Button.new() + btn_up.text = "Eine Ebene höher (Ebene " + str(dungeon_level - 1) + ")" + btn_up.pressed.connect(_on_portal_go_up) + vbox.add_child(btn_up) + + var btn_out = Button.new() + btn_out.text = "Zurück zur Oberwelt" + btn_out.pressed.connect(_on_portal_exit) + vbox.add_child(btn_out) + + var btn_cancel = Button.new() + btn_cancel.text = "Abbrechen" + btn_cancel.pressed.connect(_on_portal_cancel) + vbox.add_child(btn_cancel) + + portal_choice_panel.add_child(vbox) + player.hud.add_child(portal_choice_panel) + portal_choice_panel.anchors_preset = Control.PRESET_CENTER + portal_choice_panel.position -= portal_choice_panel.size / 2 + +func _on_portal_go_up(): + _close_portal_choice() + _go_up() + +func _on_portal_exit(): + _close_portal_choice() + _exit_dungeon() + +func _on_portal_cancel(): + _close_portal_choice() + +func _close_portal_choice(): + if portal_choice_panel and is_instance_valid(portal_choice_panel): + portal_choice_panel.queue_free() + portal_choice_panel = null + get_tree().paused = false + +func _hide_overworld(): + overworld_nodes.clear() + for child in get_children(): + if child == player: + continue + if child.name == "OverworldEnv": + continue + if child is Node3D or child is StaticBody3D: + overworld_nodes.append(child) + child.visible = false + if child is StaticBody3D: + child.process_mode = Node.PROCESS_MODE_DISABLED + if overworld_env: + overworld_env_resource = overworld_env.environment + overworld_env.environment = null + +func _generate_dungeon_level(): + dungeon_container = Node3D.new() + dungeon_container.name = "DungeonContainer" + add_child(dungeon_container) + + # Gespeichertes Level laden oder neu generieren + if saved_dungeons.has(dungeon_level): + dungeon_grid = saved_dungeons[dungeon_level]["grid"].duplicate(true) + dungeon_rooms = saved_dungeons[dungeon_level]["rooms"].duplicate(true) + else: + _generate_dungeon() + + _build_dungeon_geometry() + _build_dungeon_navmesh() + + # Edge-Connection-Margin erhöhen damit NavMesh-Zellen sich verbinden + var map_rid = get_world_3d().navigation_map + NavigationServer3D.map_set_edge_connection_margin(map_rid, 0.6) + + _place_return_portal() + _place_deeper_portal() + _setup_dungeon_lighting() + _setup_dungeon_environment() + + # Spieler-Spawn: beim Hochgehen im letzten Raum, sonst im ersten + var spawn_room: Rect2i + if saved_dungeons.has(dungeon_level) and dungeon_level < _get_max_visited_level(): + # Von unten zurückgekommen → Spawn am Deeper-Portal (letzter Raum) + spawn_room = dungeon_rooms[dungeon_rooms.size() - 1] + else: + # Neu betreten oder von oben → Spawn am Eingang (erster Raum) + spawn_room = dungeon_rooms[0] + + var spawn = Vector3( + (spawn_room.position.x + spawn_room.size.x / 2.0) * TILE_SIZE, + 0.5, + (spawn_room.position.y + spawn_room.size.y / 2.0) * TILE_SIZE + ) + player.global_position = spawn + + # Gegner im Dungeon spawnen + _spawn_dungeon_enemies() + +func _get_max_visited_level() -> int: + var max_level = 0 + for key in saved_dungeons.keys(): + if key > max_level: + max_level = key + return max_level + +func _clear_dungeon(): + if dungeon_container: + dungeon_container.queue_free() + dungeon_container = null + if dungeon_env: + dungeon_env.queue_free() + dungeon_env = null + return_portal_area = null + return_portal_label = null + deeper_portal_area = null + deeper_portal_label = null + dungeon_grid.clear() + dungeon_rooms.clear() + +func _exit_dungeon(): + in_dungeon = false + dungeon_level = 0 + _clear_dungeon() + saved_dungeons.clear() + + # Overworld-Nodes wieder zeigen + for node in overworld_nodes: + if is_instance_valid(node): + node.visible = true + if node is StaticBody3D: + node.process_mode = Node.PROCESS_MODE_INHERIT + overworld_nodes.clear() + + if overworld_env: + overworld_env.environment = overworld_env_resource + + # Spieler zurück zur Overworld + player.global_position = player_overworld_pos + +# ───────────────────────────────────────────────────────────────────────────── +# DUNGEON-GENERIERUNG +# ───────────────────────────────────────────────────────────────────────────── + +func _generate_dungeon(): + dungeon_grid.clear() + dungeon_rooms.clear() + + for x in range(GRID_WIDTH): + var col = [] + for y in range(GRID_HEIGHT): + col.append(0) + dungeon_grid.append(col) + + var attempts = 0 + var target_rooms = randi_range(MIN_ROOMS, MAX_ROOMS) + while dungeon_rooms.size() < target_rooms and attempts < 200: + attempts += 1 + var w = randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) + var h = randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) + var x = randi_range(2, GRID_WIDTH - w - 2) + var y = randi_range(2, GRID_HEIGHT - h - 2) + var new_room = Rect2i(x, y, w, h) + if _dungeon_room_fits(new_room): + dungeon_rooms.append(new_room) + _carve_room(new_room) + + for i in range(dungeon_rooms.size() - 1): + _carve_corridor(dungeon_rooms[i], dungeon_rooms[i + 1]) + +func _dungeon_room_fits(new_room: Rect2i) -> bool: + var expanded = Rect2i(new_room.position - Vector2i(2, 2), new_room.size + Vector2i(4, 4)) + for room in dungeon_rooms: + if expanded.intersects(room): + return false + return true + +func _carve_room(room: Rect2i): + for x in range(room.position.x, room.position.x + room.size.x): + for y in range(room.position.y, room.position.y + room.size.y): + dungeon_grid[x][y] = 1 + +func _carve_corridor(room_a: Rect2i, room_b: Rect2i): + var ax = room_a.position.x + room_a.size.x / 2 + var ay = room_a.position.y + room_a.size.y / 2 + var bx = room_b.position.x + room_b.size.x / 2 + var by = room_b.position.y + room_b.size.y / 2 + + var corridor_width = 2 + var start_x = min(ax, bx) + var end_x = max(ax, bx) + for x in range(start_x, end_x + 1): + for w in range(corridor_width): + if x >= 0 and x < GRID_WIDTH and ay + w >= 0 and ay + w < GRID_HEIGHT: + dungeon_grid[x][ay + w] = 1 + + var start_y = min(ay, by) + var end_y = max(ay, by) + for y in range(start_y, end_y + 1): + for w in range(corridor_width): + if bx + w >= 0 and bx + w < GRID_WIDTH and y >= 0 and y < GRID_HEIGHT: + dungeon_grid[bx + w][y] = 1 + +func _build_dungeon_geometry(): + var floor_mat = StandardMaterial3D.new() + floor_mat.albedo_color = Color(0.25, 0.22, 0.2) + floor_mat.roughness = 0.9 + + var wall_mat = StandardMaterial3D.new() + wall_mat.albedo_color = Color(0.35, 0.3, 0.28) + wall_mat.roughness = 0.95 + + var ceiling_mat = StandardMaterial3D.new() + ceiling_mat.albedo_color = Color(0.2, 0.18, 0.16) + ceiling_mat.roughness = 0.95 + + for x in range(GRID_WIDTH): + for y in range(GRID_HEIGHT): + if dungeon_grid[x][y] == 1: + # Boden + var floor_tile = CSGBox3D.new() + floor_tile.size = Vector3(TILE_SIZE, 0.3, TILE_SIZE) + floor_tile.position = Vector3(x * TILE_SIZE, -0.15, y * TILE_SIZE) + floor_tile.material = floor_mat + floor_tile.use_collision = true + dungeon_container.add_child(floor_tile) + + # Decke + var ceil_tile = CSGBox3D.new() + ceil_tile.size = Vector3(TILE_SIZE, 0.3, TILE_SIZE) + ceil_tile.position = Vector3(x * TILE_SIZE, WALL_HEIGHT + 0.15, y * TILE_SIZE) + ceil_tile.material = ceiling_mat + dungeon_container.add_child(ceil_tile) + + # Wände + var directions = [ + Vector2i(-1, 0), Vector2i(1, 0), + Vector2i(0, -1), Vector2i(0, 1) + ] + for dir in directions: + var nx = x + dir.x + var ny = y + dir.y + if nx < 0 or nx >= GRID_WIDTH or ny < 0 or ny >= GRID_HEIGHT or dungeon_grid[nx][ny] == 0: + var wall = CSGBox3D.new() + if dir.x != 0: + wall.size = Vector3(0.3, WALL_HEIGHT, TILE_SIZE) + wall.position = Vector3( + x * TILE_SIZE + dir.x * TILE_SIZE / 2.0, + WALL_HEIGHT / 2.0, + y * TILE_SIZE + ) + else: + wall.size = Vector3(TILE_SIZE, WALL_HEIGHT, 0.3) + wall.position = Vector3( + x * TILE_SIZE, + WALL_HEIGHT / 2.0, + y * TILE_SIZE + dir.y * TILE_SIZE / 2.0 + ) + wall.material = wall_mat + wall.use_collision = true + dungeon_container.add_child(wall) + +func _place_return_portal(): + var first = dungeon_rooms[0] + var portal_pos = Vector3( + (first.position.x + 1.0) * TILE_SIZE, + 0, + (first.position.y + 1.0) * TILE_SIZE + ) + + var portal = Node3D.new() + portal.name = "ReturnPortal" + portal.position = portal_pos + dungeon_container.add_child(portal) + + var portal_mat = StandardMaterial3D.new() + portal_mat.albedo_color = Color(0.2, 0.4, 1.0) + portal_mat.emission_enabled = true + portal_mat.emission = Color(0.2, 0.4, 1.0) + portal_mat.emission_energy_multiplier = 2.0 + + # Portal-Ring + var frame = CSGTorus3D.new() + frame.inner_radius = 1.2 + frame.outer_radius = 1.6 + frame.ring_sides = 16 + frame.sides = 24 + frame.position = Vector3(0, 2.0, 0) + frame.material = portal_mat + portal.add_child(frame) + + # Portal-Fläche + var surface = CSGCylinder3D.new() + surface.radius = 1.2 + surface.height = 0.1 + surface.sides = 24 + surface.position = Vector3(0, 2.0, 0) + surface.material = portal_mat + portal.add_child(surface) + + # Trigger + return_portal_area = Area3D.new() + return_portal_area.name = "PortalArea" + return_portal_area.position = Vector3(0, 1.5, 0) + portal.add_child(return_portal_area) + + var col = CollisionShape3D.new() + var shape = BoxShape3D.new() + shape.size = Vector3(4, 4, 4) + col.shape = shape + return_portal_area.add_child(col) + + # Label + return_portal_label = Label3D.new() + return_portal_label.text = "Zurück zur Oberwelt [E]" + return_portal_label.position = Vector3(0, 4.5, 0) + return_portal_label.font_size = 48 + return_portal_label.modulate = Color(0.3, 0.6, 1.0) + return_portal_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED + return_portal_label.visible = false + portal.add_child(return_portal_label) + +func _place_deeper_portal(): + var last = dungeon_rooms[dungeon_rooms.size() - 1] + var portal_pos = Vector3( + (last.position.x + last.size.x / 2.0) * TILE_SIZE, + 0, + (last.position.y + last.size.y / 2.0) * TILE_SIZE + ) + + var portal = Node3D.new() + portal.name = "DeeperPortal" + portal.position = portal_pos + dungeon_container.add_child(portal) + + var portal_mat = StandardMaterial3D.new() + portal_mat.albedo_color = Color(1.0, 0.3, 0.1) + portal_mat.emission_enabled = true + portal_mat.emission = Color(1.0, 0.3, 0.1) + portal_mat.emission_energy_multiplier = 2.0 + + # Portal-Ring (rot/orange) + var frame = CSGTorus3D.new() + frame.inner_radius = 1.2 + frame.outer_radius = 1.6 + frame.ring_sides = 16 + frame.sides = 24 + frame.position = Vector3(0, 2.0, 0) + frame.material = portal_mat + portal.add_child(frame) + + # Portal-Fläche + var surface = CSGCylinder3D.new() + surface.radius = 1.2 + surface.height = 0.1 + surface.sides = 24 + surface.position = Vector3(0, 2.0, 0) + surface.material = portal_mat + portal.add_child(surface) + + # Trigger + deeper_portal_area = Area3D.new() + deeper_portal_area.name = "DeeperPortalArea" + deeper_portal_area.position = Vector3(0, 1.5, 0) + portal.add_child(deeper_portal_area) + + var col = CollisionShape3D.new() + var shape = BoxShape3D.new() + shape.size = Vector3(4, 4, 4) + col.shape = shape + deeper_portal_area.add_child(col) + + # Label + deeper_portal_label = Label3D.new() + deeper_portal_label.text = "Ebene " + str(dungeon_level + 1) + " betreten [E]" + deeper_portal_label.position = Vector3(0, 4.5, 0) + deeper_portal_label.font_size = 48 + deeper_portal_label.modulate = Color(1.0, 0.5, 0.2) + deeper_portal_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED + deeper_portal_label.visible = false + portal.add_child(deeper_portal_label) + +func _setup_dungeon_lighting(): + # Licht in jedem Raum + for room in dungeon_rooms: + var center = Vector3( + (room.position.x + room.size.x / 2.0) * TILE_SIZE, + WALL_HEIGHT - 1.0, + (room.position.y + room.size.y / 2.0) * TILE_SIZE + ) + var light = OmniLight3D.new() + light.position = center + light.omni_range = 15.0 + light.light_energy = 1.2 + light.light_color = Color(1.0, 0.85, 0.6) + light.shadow_enabled = true + dungeon_container.add_child(light) + + # Korridor-Beleuchtung + for i in range(dungeon_rooms.size() - 1): + var room_a = dungeon_rooms[i] + var room_b = dungeon_rooms[i + 1] + var ax = (room_a.position.x + room_a.size.x / 2.0) * TILE_SIZE + var ay = (room_a.position.y + room_a.size.y / 2.0) * TILE_SIZE + var bx = (room_b.position.x + room_b.size.x / 2.0) * TILE_SIZE + var by = (room_b.position.y + room_b.size.y / 2.0) * TILE_SIZE + + # Licht am L-Knick + var corner_light = OmniLight3D.new() + corner_light.position = Vector3(bx, WALL_HEIGHT - 1.0, ay) + corner_light.omni_range = 12.0 + corner_light.light_energy = 1.0 + corner_light.light_color = Color(1.0, 0.8, 0.5) + dungeon_container.add_child(corner_light) + + # Mitte horizontaler Gang + var mid_h = OmniLight3D.new() + mid_h.position = Vector3((ax + bx) / 2.0, WALL_HEIGHT - 1.0, ay) + mid_h.omni_range = 10.0 + mid_h.light_energy = 0.8 + mid_h.light_color = Color(1.0, 0.8, 0.5) + dungeon_container.add_child(mid_h) + + # Mitte vertikaler Gang + var mid_v = OmniLight3D.new() + mid_v.position = Vector3(bx, WALL_HEIGHT - 1.0, (ay + by) / 2.0) + mid_v.omni_range = 10.0 + mid_v.light_energy = 0.8 + mid_v.light_color = Color(1.0, 0.8, 0.5) + dungeon_container.add_child(mid_v) + +func _setup_dungeon_environment(): + var env = Environment.new() + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.02, 0.02, 0.04) + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.08, 0.07, 0.1) + env.ambient_light_energy = 0.3 + env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + env.fog_enabled = true + env.fog_light_color = Color(0.05, 0.05, 0.08) + env.fog_density = 0.02 + + dungeon_env = WorldEnvironment.new() + dungeon_env.name = "DungeonEnv" + dungeon_env.environment = env + add_child(dungeon_env) + +func _build_dungeon_navmesh(): + var nav_region = NavigationRegion3D.new() + nav_region.name = "DungeonNavRegion" + var nav_mesh = NavigationMesh.new() + + var half = TILE_SIZE / 2.0 + var margin = 0.5 # Abstand zu Wänden + + # Positions-basierte Vertex-Deduplizierung: + # Gleiche Position → gleicher Index → NavMesh ist automatisch zusammenhängend + var pos_to_idx: Dictionary = {} # "x_z" String → vertex index + var vertices = PackedVector3Array() + var cell_indices: Array = [] # [{i0, i1, i2, i3}, ...] + + for x in range(GRID_WIDTH): + for y in range(GRID_HEIGHT): + if dungeon_grid[x][y] != 1: + continue + + var wall_left = (x <= 0 or dungeon_grid[x - 1][y] == 0) + var wall_right = (x >= GRID_WIDTH - 1 or dungeon_grid[x + 1][y] == 0) + var wall_top = (y <= 0 or dungeon_grid[x][y - 1] == 0) + var wall_bot = (y >= GRID_HEIGHT - 1 or dungeon_grid[x][y + 1] == 0) + + var lx = x * TILE_SIZE - half + (margin if wall_left else 0.0) + var rx = (x + 1) * TILE_SIZE - half - (margin if wall_right else 0.0) + var tz = y * TILE_SIZE - half + (margin if wall_top else 0.0) + var bz = (y + 1) * TILE_SIZE - half - (margin if wall_bot else 0.0) + + var corners = [ + Vector3(lx, 0.0, tz), Vector3(rx, 0.0, tz), + Vector3(rx, 0.0, bz), Vector3(lx, 0.0, bz) + ] + + var indices = PackedInt32Array() + for c in corners: + var key = "%0.4f_%0.4f" % [c.x, c.z] + if not pos_to_idx.has(key): + pos_to_idx[key] = vertices.size() + vertices.append(c) + indices.append(pos_to_idx[key]) + cell_indices.append(indices) + + nav_mesh.vertices = vertices + for ci in cell_indices: + nav_mesh.add_polygon(ci) + + nav_region.navigation_mesh = nav_mesh + dungeon_container.add_child(nav_region) + +func _spawn_dungeon_enemies(): + for i in range(1, dungeon_rooms.size()): + # Letzter Raum = Deeper-Portal, keine Gegner + if i == dungeon_rooms.size() - 1: + continue + var room = dungeon_rooms[i] + var center = Vector3( + (room.position.x + room.size.x / 2.0) * TILE_SIZE, + 0.5, + (room.position.y + room.size.y / 2.0) * TILE_SIZE + ) + var enemy = ENEMY_SCENE.instantiate() + dungeon_container.add_child(enemy) + enemy.global_position = center + # Gegner-Level = Dungeon-Ebene + enemy.mob_level = dungeon_level + _setup_enemy(enemy) + +# ───────────────────────────────────────────────────────────────────────────── +# MENÜ & SPIELER-SETUP +# ───────────────────────────────────────────────────────────────────────────── + # Nach Hauptmenü: Klassenauswahl anzeigen func _on_start_game(): var menu = CLASS_SELECTION_MENU.instantiate() add_child(menu) menu.class_selected.connect(_on_class_selected) - # Spiel wieder pausieren für Klassenauswahl get_tree().paused = true # Klasse ausgewählt: Spieler initialisieren @@ -120,7 +784,10 @@ func _on_class_selected(character_class: CharacterClass): if child.has_method("take_damage") and child != player: _setup_enemy(child) -# Gegner initialisieren und Signal verbinden +# ───────────────────────────────────────────────────────────────────────────── +# GEGNER-SYSTEM +# ───────────────────────────────────────────────────────────────────────────── + func _setup_enemy(enemy): if enemy and player: enemy.target = player @@ -130,26 +797,26 @@ func _setup_enemy(enemy): 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!") -# Loot-Drop an Spieler weiterleiten func _on_enemy_dropped_loot(loot: Dictionary, world_pos: Vector3): if player: player.receive_loot(loot, world_pos) -# Gegner gestorben: Nach 5 Sekunden respawnen 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) + if in_dungeon and dungeon_container: + var new_enemy = ENEMY_SCENE.instantiate() + dungeon_container.add_child(new_enemy) + new_enemy.global_position = spawn_position + _setup_enemy(new_enemy) + elif not in_dungeon: + _spawn_enemy(spawn_position) -# Neuen Gegner an Position spawnen func _spawn_enemy(position: Vector3): var new_enemy = ENEMY_SCENE.instantiate() add_child(new_enemy) new_enemy.global_position = position _setup_enemy(new_enemy) - print("Gegner respawned!") diff --git a/world.tscn b/world.tscn index ce721c1..5ec9635 100644 --- a/world.tscn +++ b/world.tscn @@ -4,34 +4,337 @@ [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_floor"] size = Vector3(200, 0.5, 200) -[sub_resource type="BoxMesh" id="BoxMesh_tlwt5"] +[sub_resource type="BoxMesh" id="BoxMesh_floor"] size = Vector3(200, 0.5, 200) [sub_resource type="NavigationMesh" id="NavigationMesh_fj7yv"] -[node name="World" type="Node3D" unique_id=2007838514] +[sub_resource type="StandardMaterial3D" id="mat_rock"] +albedo_color = Color(0.4, 0.38, 0.35, 1) +roughness = 0.95 + +[sub_resource type="StandardMaterial3D" id="mat_stone"] +albedo_color = Color(0.3, 0.28, 0.25, 1) +roughness = 0.9 + +[sub_resource type="StandardMaterial3D" id="mat_entrance"] +albedo_color = Color(0.02, 0.02, 0.02, 1) + +[sub_resource type="BoxShape3D" id="BoxShape3D_gate"] +size = Vector3(5, 4, 4) + +[sub_resource type="StandardMaterial3D" id="mat_rock_small"] +albedo_color = Color(0.45, 0.42, 0.38, 1) +roughness = 0.95 + +[sub_resource type="StandardMaterial3D" id="mat_trunk"] +albedo_color = Color(0.35, 0.22, 0.1, 1) +roughness = 0.9 + +[sub_resource type="StandardMaterial3D" id="mat_leaf"] +albedo_color = Color(0.15, 0.45, 0.12, 1) +roughness = 0.85 + +[node name="World" type="Node3D" unique_id=1518976304] script = ExtResource("1_tlwt5") -[node name="Boden" type="StaticBody3D" parent="." unique_id=2101916269] +[node name="Boden" type="StaticBody3D" parent="." unique_id=1937472568] -[node name="CollisionShape3D" type="CollisionShape3D" parent="Boden" unique_id=1873339390] -shape = SubResource("BoxShape3D_fj7yv") +[node name="CollisionShape3D" type="CollisionShape3D" parent="Boden" unique_id=1499976920] +shape = SubResource("BoxShape3D_floor") -[node name="MeshInstance3D" type="MeshInstance3D" parent="Boden" unique_id=1214783061] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2) -mesh = SubResource("BoxMesh_tlwt5") +[node name="MeshInstance3D" type="MeshInstance3D" parent="Boden" unique_id=1316024844] +mesh = SubResource("BoxMesh_floor") -[node name="Player" parent="." unique_id=937297102 instance=ExtResource("1_f3sb7")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4345045, 0) +[node name="Player" parent="." unique_id=841198255 instance=ExtResource("1_f3sb7")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.435, 0) -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1394887598] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1642167272] 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=1093030835] 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) +[node name="enemy" parent="." unique_id=1435537835 instance=ExtResource("3_enemy")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.388, -36) + +[node name="Mountain" type="CSGCombiner3D" parent="." unique_id=677409308] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -80) +use_collision = true + +[node name="MainBody" type="CSGSphere3D" parent="Mountain" unique_id=578990011] +transform = Transform3D(1.5, 0, 0, 0, 0.6, 0, 0, 0, 1, 0, -5, 0) +radius = 30.0 +radial_segments = 24 +rings = 16 +material = SubResource("mat_rock") + +[node name="Peak" type="CSGSphere3D" parent="Mountain" unique_id=1930092098] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 0) +radius = 15.0 +radial_segments = 16 +rings = 12 +material = SubResource("mat_rock") + +[node name="LeftHill" type="CSGSphere3D" parent="Mountain" unique_id=1517453171] +transform = Transform3D(1.2, 0, 0, 0, 0.5, 0, 0, 0, 0.8, -25, -8, 5) +radius = 20.0 +radial_segments = 16 +rings = 12 +material = SubResource("mat_rock") + +[node name="RightHill" type="CSGSphere3D" parent="Mountain" unique_id=334184404] +transform = Transform3D(1, 0, 0, 0, 0.45, 0, 0, 0, 0.9, 22, -8, 8) +radius = 18.0 +radial_segments = 16 +rings = 12 +material = SubResource("mat_rock") + +[node name="DungeonGate" type="Node3D" parent="." unique_id=1787139888] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -45) + +[node name="LeftPillar" type="CSGBox3D" parent="DungeonGate" unique_id=975630240] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.5, 3, 0) +use_collision = true +size = Vector3(1.5, 6, 1.5) +material = SubResource("mat_stone") + +[node name="RightPillar" type="CSGBox3D" parent="DungeonGate" unique_id=682177996] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.5, 3, 0) +use_collision = true +size = Vector3(1.5, 6, 1.5) +material = SubResource("mat_stone") + +[node name="TopBar" type="CSGBox3D" parent="DungeonGate" unique_id=1917431148] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.6, 0) +use_collision = true +size = Vector3(6.5, 1.2, 1.5) +material = SubResource("mat_stone") + +[node name="Entrance" type="CSGBox3D" parent="DungeonGate" unique_id=782262731] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.75, -0.5) +size = Vector3(3.5, 5.5, 2) +material = SubResource("mat_entrance") + +[node name="GateArea" type="Area3D" parent="DungeonGate" unique_id=1947692918] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 2) + +[node name="CollisionShape3D" type="CollisionShape3D" parent="DungeonGate/GateArea" unique_id=1018035780] +shape = SubResource("BoxShape3D_gate") + +[node name="GateLabel" type="Label3D" parent="DungeonGate" unique_id=1720057230] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 7.5, 1) +visible = false +billboard = 1 +modulate = Color(1, 0.85, 0.3, 1) +text = "Dungeon betreten [E]" +font_size = 48 + +[node name="Rock1" type="CSGSphere3D" parent="." unique_id=427636709] +transform = Transform3D(1.1, 0, 0, 0, 0.5, 0, 0, 0, 0.9, 15, 0.5, -20) +radius = 1.8 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock2" type="CSGSphere3D" parent="." unique_id=176525307] +transform = Transform3D(0.9, 0, 0, 0, 0.6, 0, 0, 0, 1.1, -18, 0.4, -15) +radius = 1.4 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock3" type="CSGSphere3D" parent="." unique_id=1182279621] +transform = Transform3D(1.2, 0, 0, 0, 0.45, 0, 0, 0, 0.8, 25, 0.7, 10) +radius = 2.2 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock4" type="CSGSphere3D" parent="." unique_id=1966368230] +transform = Transform3D(0.8, 0, 0, 0, 0.55, 0, 0, 0, 1, -22, 0.3, 25) +radius = 1.0 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock5" type="CSGSphere3D" parent="." unique_id=48323199] +transform = Transform3D(1.3, 0, 0, 0, 0.5, 0, 0, 0, 0.9, 30, 0.6, -40) +radius = 2.0 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock6" type="CSGSphere3D" parent="." unique_id=604365640] +transform = Transform3D(1, 0, 0, 0, 0.7, 0, 0, 0, 1.2, -28, 0.5, -45) +radius = 1.6 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock7" type="CSGSphere3D" parent="." unique_id=1821485412] +transform = Transform3D(0.9, 0, 0, 0, 0.4, 0, 0, 0, 1, 12, 0.3, 30) +radius = 1.2 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock8" type="CSGSphere3D" parent="." unique_id=1387702407] +transform = Transform3D(1.1, 0, 0, 0, 0.6, 0, 0, 0, 0.85, -10, 0.6, -50) +radius = 2.4 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Rock9" type="CSGSphere3D" parent="." unique_id=1450102956] +transform = Transform3D(1, 0, 0, 0, 0.5, 0, 0, 0, 1.1, 35, 0.4, -25) +radius = 1.5 +radial_segments = 8 +material = SubResource("mat_rock_small") + +[node name="Tree1" type="Node3D" parent="." unique_id=618772270] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 20, 0, -10) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree1" unique_id=1186519127] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.75, 0) +radius = 0.3 +height = 5.5 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree1" unique_id=2089075716] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.3, 4.2, -0.2) +radius = 2.2 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree1" unique_id=2070989054] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.4, 5.4, 0.3) +radius = 2.0 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Tree2" type="Node3D" parent="." unique_id=1889546293] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -30) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree2" unique_id=997380154] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.25, 0) +radius = 0.3 +height = 6.5 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree2" unique_id=809808235] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 5, 0.2) +radius = 2.5 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree2" unique_id=481648384] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.3, 6.2, -0.4) +radius = 2.2 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown3" type="CSGSphere3D" parent="Tree2" unique_id=184574046] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1, 7.4, 0.1) +radius = 1.8 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Tree3" type="Node3D" parent="." unique_id=1385340799] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 28, 0, 20) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree3" unique_id=387398909] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.25, 0) +radius = 0.3 +height = 4.5 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree3" unique_id=1139907459] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.2, 3.5, 0.5) +radius = 2.0 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree3" unique_id=391069386] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.4, 4.7, -0.3) +radius = 2.3 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Tree4" type="Node3D" parent="." unique_id=613675839] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0, 15) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree4" unique_id=220117087] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 0) +radius = 0.3 +height = 6.0 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree4" unique_id=129254377] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6, 4.5, -0.1) +radius = 2.4 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree4" unique_id=929192645] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 5.7, 0.4) +radius = 2.1 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Tree5" type="Node3D" parent="." unique_id=94347379] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, 35) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree5" unique_id=199856993] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.5, 0) +radius = 0.3 +height = 7.0 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree5" unique_id=1965121271] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.3, 5.3, 0.6) +radius = 2.6 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree5" unique_id=1961694117] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 6.5, -0.2) +radius = 2.3 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown3" type="CSGSphere3D" parent="Tree5" unique_id=1346853660] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 7.7, 0.3) +radius = 1.9 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Tree6" type="Node3D" parent="." unique_id=1309473387] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -30, 0, -10) + +[node name="Trunk" type="CSGCylinder3D" parent="Tree6" unique_id=1889196546] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) +radius = 0.3 +height = 5.0 +material = SubResource("mat_trunk") + +[node name="Crown1" type="CSGSphere3D" parent="Tree6" unique_id=1159575861] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2, 3.8, -0.4) +radius = 2.1 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf") + +[node name="Crown2" type="CSGSphere3D" parent="Tree6" unique_id=2073540260] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 5, 0.2) +radius = 2.4 +radial_segments = 10 +rings = 8 +material = SubResource("mat_leaf")