diff --git a/PROJEKTDOKU.md b/PROJEKTDOKU.md
index 5b714c5..d7edc14 100644
--- a/PROJEKTDOKU.md
+++ b/PROJEKTDOKU.md
@@ -23,7 +23,10 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge
| RMB gehalten | Kamera drehen, Spieler schaut mit |
| Linksklick auf Gegner | Ziel markieren |
| Rechtsklick auf Gegner | Ziel markieren + Autoattack starten |
-| 1 – 9 | Aktionsleiste Slot auswählen |
+| 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 |
@@ -32,7 +35,7 @@ Gegner bekämpfen und ihre Charaktere mit verschiedenen Klassen und Ausrüstunge
## Szenen-Struktur
### world.tscn
-Hauptszene der Spielwelt.
+Hauptszene der Spielwelt. Zeigt bei Start das Hauptmenü (Einstellungen), dann Klassenauswahl.
```
World (Node3D)
├── Player (player.tscn)
@@ -45,21 +48,25 @@ World (Node3D)
```
### player.tscn
-Der Spielercharakter.
+Der Spielercharakter mit allen UI-Panels.
```
Player (CharacterBody3D)
-├── [Kenney GLB Modell]
+├── PlayerModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer)
├── CollisionShape3D
├── CameraPivot (Node3D)
│ └── Camera3D
-└── HUD (hud.tscn)
+├── HUD (hud.tscn)
+├── CharacterPanel (character_panel.tscn)
+├── InventoryPanel (inventory_panel.tscn)
+├── LootWindow (loot_window.tscn)
+└── SkillPanel (skill_panel.tscn)
```
### enemy.tscn
-Ein Gegner.
+Ein Gegner mit Level-basiertem Stats-System.
```
Enemy (CharacterBody3D)
-├── [Kenney GLB Modell]
+├── EnemyModel (warrior.fbx — Mixamo Charakter mit Skeleton + AnimationPlayer)
├── CollisionShape3D
├── Area3D
│ └── CollisionShape3D
@@ -67,188 +74,414 @@ Enemy (CharacterBody3D)
└── HealthLabel (Label3D)
```
-### hud.tscn
-Die Spieler-UI.
-```
-HUD (CanvasLayer)
-└── Control
- ├── HealthBar (ProgressBar)
- │ └── HealthLabel (Label)
- └── ActionBar (HBoxContainer)
- └── A1 – A9 (Panel)
- └── Label (1–9)
-```
-
---
-## Scripts
+## Klassen-System
-### player.gd
-Steuert den Spielercharakter.
-
-**Variablen:**
-| Variable | Typ | Beschreibung |
-|---|---|---|
-| SPEED | const float | Bewegungsgeschwindigkeit (5.0) |
-| JUMP_VELOCITY | const float | Sprungkraft (4.5) |
-| GRAVITY | const float | Schwerkraft (9.8) |
-| max_hp | int | Maximale HP (100) |
-| current_hp | int | Aktuelle HP |
-| can_attack | bool | Angriff möglich (Cooldown-Flag) |
-| target | Node | Aktuell markierter Gegner |
-| equipped_weapon | Weapon | Ausgerüstete Waffe (null = unbewaffnet) |
-
-**Funktionen:**
-| Funktion | Beschreibung |
-|---|---|
-| take_damage(amount) | Schaden abziehen, HP-Leiste aktualisieren |
-| heal(amount) | HP heilen |
-| die() | Spieler gestorben |
-| get_attack_damage() | Schaden berechnen (Waffe oder 1) |
-| get_attack_range() | Reichweite (Waffe oder 1.5) |
-| get_attack_cooldown() | Cooldown (Waffe oder 1.5s) |
-| set_target(target, start_attack) | Ziel markieren, optional Autoattack starten |
-| autoattack() | Wiederholt angreifen solange Ziel gültig |
-| _try_select_target(start_attack) | Raycast von Kamera auf Mausposition |
-
----
-
-### enemy.gd
-Steuert den Gegner.
-
-**Variablen:**
-| Variable | Typ | Beschreibung |
-|---|---|---|
-| SPEED | const float | Bewegungsgeschwindigkeit (3.0) |
-| ATTACK_DAMAGE | const int | Schaden pro Angriff (5) |
-| ATTACK_RANGE | const float | Angriffsreichweite (1.5) |
-| ATTACK_COOLDOWN | const float | Angriffspause (2.0s) |
-| max_hp | int | Maximale HP (50) |
-| target | Node | Ziel des Gegners (Spieler) |
-
-**Funktionen:**
-| Funktion | Beschreibung |
-|---|---|
-| take_damage(amount) | Schaden nehmen, bei 0 HP sterben |
-| die() | Gegner aus Szene entfernen |
-| show_health() | HP-Label einblenden |
-| hide_health() | HP-Label ausblenden |
-| _attack() | Angriff mit Cooldown |
-
----
-
-### camera_pivot.gd
-Steuert die Third-Person-Kamera.
-
-**Einstellbare Parameter (Export):**
-| Parameter | Standard | Beschreibung |
-|---|---|---|
-| sensitivity | 0.3 | Mausempfindlichkeit |
-| min_pitch | -40° | Maximale Neigung nach unten |
-| max_pitch | 20° | Maximale Neigung nach oben |
-| min_zoom | 5.0 | Minimale Kameraentfernung |
-| max_zoom | 20.0 | Maximale Kameraentfernung |
-| zoom_speed | 1.0 | Zoom pro Mausrad-Schritt |
-
----
-
-### hud.gd
-Verwaltet die Spieler-UI.
-
-**Funktionen:**
-| Funktion | Beschreibung |
-|---|---|
-| update_health(current, max) | HP-Leiste und Text aktualisieren |
-| set_active_slot(index) | Aktions-Slot 0.1s golden hervorheben |
-
----
-
-## Ressourcen-Klassen
-
-### Weapon (resources/weapon.gd)
-Definiert eine Waffe als Godot-Ressource (.tres).
+### CharacterClass (character_class.gd)
+Resource-Klasse die eine spielbare Klasse definiert.
| Property | Typ | Beschreibung |
|---|---|---|
-| name | String | Waffenname |
-| weapon_type | WeaponType | UNARMED, SWORD, AXE, MACE, DAGGER, STAFF, BOW |
-| min_damage | int | Minimaler Schaden |
-| max_damage | int | Maximaler Schaden |
-| attack_speed | float | Angriffsgeschwindigkeit (Cooldown) |
-| range | float | Angriffsreichweite |
-| icon | Texture2D | Icon für die Aktionsleiste |
+| class_name_de | String | Deutscher Klassenname |
+| main_stat | MainStat | STRENGTH, AGILITY oder INTELLIGENCE |
+| base_strength/agility/intelligence/stamina | int | Basis-Stats auf Level 1 |
+| strength/agility/intelligence/stamina_per_level | float | Stat-Zuwachs pro Level |
+| unarmed_min_damage | int | Unbewaffneter Mindestschaden |
+| unarmed_max_damage | int | Unbewaffneter Maximalschaden |
+| unarmed_attack_speed | float | Unbewaffnete Angriffsgeschwindigkeit |
+| resource_type | ResourceType | NONE, MANA, RAGE, ENERGY |
+| base_resource | int | Basis-Ressource auf Level 1 |
-### Attack (resources/attack.gd)
-Definiert einen Angriff — Schaden kommt von der ausgerüsteten Waffe.
+**Konstanten:**
+- `HP_PER_STAMINA = 10` — HP pro Stamina-Punkt
+- `MANA_PER_INT = 5` — Mana pro Intelligenz-Punkt
+- `DAMAGE_PER_MAIN_STAT = 0.5` — Zusatzschaden pro Main-Stat-Punkt
+
+### Verfügbare Klassen (classes/)
+
+| Klasse | Main-Stat | Ressource | STR | AGI | INT | STA | Unbewaffnet |
+|---|---|---|---|---|---|---|---|
+| Krieger | Stärke | Keine | 15 | 8 | 5 | 12 | 2-4 Schaden, 1.8s |
+| Schurke | Beweglichkeit | Energie (100) | 8 | 15 | 7 | 10 | 1-3 Schaden, 1.5s |
+| Magier | Intelligenz | Mana (100 + INT*5) | 5 | 8 | 15 | 8 | 1-2 Schaden, 2.0s |
+
+### Hauptmenü (main_menu.gd)
+Wird als erstes angezeigt mit Optionen:
+- **Spielen** — Öffnet Klassenauswahl
+- **Einstellungen** — Auflösung (720p-4K), Fenstermodus (Fenster/Randlos/Vollbild), VSync, Anti-Aliasing (Aus/2x/4x/8x)
+- **Beenden** — Spiel schließen
+
+### Klassenauswahl (class_selection_menu.gd)
+Wird nach dem Hauptmenü angezeigt. Spieler wählt eine Klasse, danach wird Startausrüstung angelegt.
+
+---
+
+## Klassen-Ressourcen-System
+
+Jede Klasse kann eine eigene Ressource haben:
+| Ressource | Farbe | Skalierung | Klasse |
+|---|---|---|---|
+| Keine | - | - | Krieger |
+| Mana | Blau | base_resource + INT * 5 | Magier |
+| Energie | Gelb | Fix (base_resource) | Schurke |
+| Wut | Rot | Fix (base_resource) | (geplant) |
+
+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, ...)
+- **Stats pro Level:** Basierend auf Klassen-Zuwachsraten
+- **HP bei Level-Up:** Werden vollständig aufgefüllt (HP + Klassen-Ressource)
+- **Character Panel:** Aktualisiert sich automatisch bei Level-Up
+
+---
+
+## Equipment-System
+
+### Equipment (equipment.gd)
+Resource-Klasse für alle Ausrüstungsgegenstände.
+
+**Slots:** HEAD, CHEST, HANDS, LEGS, FEET, WEAPON, OFFHAND
+
+**Seltenheiten:**
+| Seltenheit | Farbe |
+|---|---|
+| COMMON | Weiß |
+| UNCOMMON | Grün |
+| RARE | Blau |
+| EPIC | Lila |
+
+**Stats auf Equipment:**
+| Property | Typ | Beschreibung |
+|---|---|---|
+| item_name | String | Name des Items |
+| slot | Slot | Equipment-Slot |
+| rarity | Rarity | Seltenheitsstufe |
+| armor | int | Rüstungswert |
+| strength/agility/intelligence/stamina | int | Stat-Boni |
+| haste | float | Angriffsgeschwindigkeits-Bonus (0.1 = 10%) |
+| min_damage / max_damage | int | Waffenschaden (nur Waffen) |
+| attack_speed | float | Angriffsgeschwindigkeit (nur Waffen) |
+| weapon_range | float | Reichweite (nur Waffen) |
+
+### Vorhandene Equipment-Items (equipment/)
+- **iron_sword.tres** — Eisenschwert (Waffe)
+- **steel_sword.tres** — Stahlschwert (Waffe, besser)
+- **leather_chest.tres** — Lederrüstung (Brust)
+- **iron_helm.tres** — Eisenhelm (Kopf)
+- **wooden_shield.tres** — Holzschild (Nebenhand)
+
+### Character Panel (character_panel.gd, C-Taste)
+Zeigt zwei Spalten:
+- **Stats-Spalte:** Stärke, Beweglichkeit, Intelligenz, Ausdauer, Rüstung, HP
+- **Equipment-Spalte:** Icon-Grid mit Tooltips bei Hover, Waffenschaden + DPS
+
+Unbewaffnet: Zeigt klassenabhängige Schadenswerte.
+
+---
+
+## Consumable-System
+
+### Consumable (consumable.gd)
+Resource-Klasse für verbrauchbare Items (Tränke, Essen, etc.).
| Property | Typ | Beschreibung |
|---|---|---|
-| name | String | Angriffsname |
-| damage_type | DamageType | PHYSICAL, FIRE, ICE, LIGHTNING, POISON |
-| icon | Texture2D | Icon für die Aktionsleiste |
+| item_name | String | Name des Tranks |
+| effect_type | EffectType | HEAL_HP, RESTORE_MANA, HEAL_AND_MANA |
+| amount | int | Heilungsmenge |
+| cooldown | float | Cooldown nach Benutzung |
+| icon | Texture2D | Icon für UI |
+| stack_size | int | Aktuelle Anzahl im Stack |
+| max_stack | int | Maximale Stack-Größe (20) |
+
+### Vorhandene Tränke (consumables/)
+- **small_hp_potion.tres** — Kleiner Heiltrank (+50 HP, 40% Drop-Chance)
+- **small_mana_potion.tres** — Kleiner Manatrank (+40 Mana, 25% Drop-Chance)
+
+### Benutzung
+- **Rechtsklick im Inventar:** Trank direkt benutzen
+- **Shift+Linksklick im Inventar:** Trank auf nächsten freien Aktionsleisten-Slot legen
+- **Taste 3-9:** Trank über Aktionsleiste benutzen
+- HP-Tränke: Nur wenn HP nicht voll, stellt HP wieder her
+- Mana-Tränke: Nur wenn Mana nicht voll, stellt Ressource wieder her
+- **Stacking:** Gleichnamige Tränke werden automatisch gestackt (max 20)
+
+---
+
+## Inventar-System
+
+### Inventory (inventory.gd)
+Resource-Klasse für das Spieler-Inventar.
+
+- **MAX_SLOTS:** 20 Inventarplätze
+- **Gold:** Währung, wird im HUD angezeigt
+- **Signals:** `inventory_changed`, `gold_changed`
+
+**Funktionen:**
+| Funktion | Beschreibung |
+|---|---|
+| add_item(item) | Item hinzufügen (false wenn voll) |
+| remove_item(item) | Item entfernen |
+| add_gold(amount) | Gold hinzufügen |
+| spend_gold(amount) | Gold ausgeben (false wenn nicht genug) |
+| is_full() | Prüfen ob Inventar voll |
+
+### Inventory Panel (inventory_panel.gd, I-Taste)
+- 5x4 Grid mit Item-Slots
+- Gold-Anzeige im Header
+- Items in Seltenheitsfarbe
+- Tooltips mit vollständigen Item-Stats
+- **Rechtsklick** auf Item: Direkt anlegen (tauscht mit aktuellem Equipment)
+
+---
+
+## Loot-System
+
+### LootTable (loot_table.gd)
+Resource die mögliche Drops eines Gegners definiert.
+
+| Property | Typ | Beschreibung |
+|---|---|---|
+| min_gold | int | Minimaler Gold-Drop |
+| max_gold | int | Maximaler Gold-Drop |
+| possible_drops | Array[LootEntry] | Mögliche Item-Drops |
+
+### LootEntry (loot_entry.gd)
+Ein einzelner Drop-Eintrag.
+
+| Property | Typ | Beschreibung |
+|---|---|---|
+| item | Resource | Das droppbare Item (Equipment oder Consumable) |
+| drop_chance | float | Wahrscheinlichkeit (0.0 - 1.0) |
+
+### Vorhandene LootTables (loot_tables/)
+- **goblin_loot.tres** — 2-8 Gold, Eisenschwert (15%), Lederrüstung (10%), Eisenhelm (10%), Heiltrank (40%), Manatrank (25%)
+- **skeleton_loot.tres** — 5-15 Gold, Stahlschwert (10%), Holzschild (12%), Eisenhelm (15%)
+
+### LootWindow (loot_window.gd)
+- Erscheint automatisch wenn ein Gegner stirbt
+- Zeigt Gold und gedropte Items
+- Einzelne Items per Klick aufheben
+- "Alles aufheben" Button
+- Gold skaliert mit Gegner-Level
---
## Kampfsystem
-### Autoattack
-- Rechtsklick auf Gegner → Ziel markieren + Autoattack startet
-- Linksklick auf Gegner → nur Ziel markieren
-- Autoattack läuft automatisch im Takt der Waffen-Angriffsgeschwindigkeit
-- Ohne Waffe: 1 Schaden, 1.5s Cooldown, 1.5 Reichweite
-- Mit Waffe: Schaden = zufällig zwischen min/max, Speed und Range von der Waffe
+### Global Cooldown (GCD)
+Alle Aktionen (Autoattack + Skills) teilen sich einen GCD.
-### Schadenstypen (geplant)
-- **PHYSICAL** – normaler Schaden
-- **FIRE** – Feuerschaden (z.B. Magier)
-- **ICE** – Eisschaden
-- **LIGHTNING** – Blitzschaden
-- **POISON** – Giftschaden (Schaden über Zeit geplant)
+**Formel:** `GCD = Waffen-Attackspeed / (1 + Haste)`
+
+Beispiel: Waffe mit 1.5s + 50% Haste → `1.5 / 1.5 = 1.0s`
+
+### Schadensberechnung
+1. **Basisschaden:** Zufällig zwischen Waffen min/max (oder unbewaffnet klassenabhängig)
+2. **Stat-Bonus:** `Main-Stat * 0.5`
+3. **Rüstungsreduktion (Nahkampf):** `Reduktion = Rüstung / (Rüstung + 50)`
+4. **Level-Differenz:** `±10% pro Level, max ±50%`
+5. **Mindestschaden:** Immer mindestens 1
+
+### DPS-Berechnung
+`DPS = (Durchschnittsschaden + Stat-Bonus) / GCD`
+
+### Skills
+
+#### Autoattack (Taste 1, Krieger/Schurke)
+- **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)
+- **Schaden:** Waffenschaden + INT (magisch, ignoriert Rüstung)
+- **Cooldown:** GCD
+- **Reichweite:** 20.0
+- Deaktiviert Autoattack, exklusiver Fernkampf-Modus
+- **Animation:** autoattack
+
+#### Frostblitz (Taste 2, Magier)
+- **Schaden:** 12-20 + INT (magisch)
+- **Castzeit:** 1.5 Sekunden (unterbrechbar durch Bewegung/Schaden/Springen)
+- **Manakosten:** 20
+- **Cooldown:** 2.5 Sekunden
+- **Reichweite:** 20.0
+- Castbar wird über der Aktionsleiste angezeigt
+
+#### Heavy Strike (Taste 2, Krieger/Schurke)
+- **Schaden:** 10-15 + Main-Stat Bonus
+- **Cooldown:** 3 Sekunden (eigener Cooldown, löst auch GCD aus)
+- **Reichweite:** 4.0
+- Aktiviert automatisch Autoattack danach
+- **Animation:** heavy_strike
+
+### UI & Icons
+- 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
+
+---
+
+## Gegner-System (enemy.gd)
+
+### Stats
+Level-basiert mit automatischer Skalierung:
+
+| Stat | Formel |
+|---|---|
+| 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 |
+
+### 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) |
+
+### Respawn
+- Gegner spawnen nach 5 Sekunden am Ursprungsort neu
+- Verwaltet durch world.gd
+
+### Loot-Drops
+- Jeder Gegner hat eine optionale `loot_table` (LootTable Resource)
+- Gold skaliert mit Gegner-Level
+- Ohne LootTable: Standard-Gold-Drop (1-3 * Level)
+
+---
+
+## HUD (hud.gd)
+
+| Element | Beschreibung |
+|---|---|
+| HealthBar | Rote HP-Leiste mit Text "aktuell / max" |
+| ResourceBar | Ressourcen-Leiste (Blau=Mana, Gelb=Energie, Rot=Wut), nur sichtbar wenn Klasse eine Ressource hat |
+| LevelLabel | "Lv X" Anzeige |
+| 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)
---
## Geplante Features
-
-### Klassen-System
-Jede Klasse hat unterschiedliche Basis-Stats und verfügbare Skills:
-
-| Klasse | Stärken | Waffen |
-|---|---|---|
-| Krieger | Hohe HP, hoher physischer Schaden | Schwert, Axt, Streitkolben |
-| Magier | Hoher Magieschaden, niedrige HP | Stab |
-| Bogenschütze | Hohe Reichweite, schnelle Angriffe | Bogen, Dolch |
-
-### Weitere geplante Features
-- [ ] Klassen-System mit unterschiedlichen Stats
-- [ ] Mana-System für Magier
+- [ ] Wut-Ressource für Krieger
+- [ ] Ressourcen-System für Gegner (nicht alle haben Mana)
- [ ] Spell-System (Feuerbälle etc.)
-- [ ] Inventar und Ausrüstungssystem
-- [ ] Waffen aufheben und ausrüsten
-- [ ] Erfahrungspunkte und Level-Up
+- [ ] Schadenstypen (Physical, Fire, Ice, Lightning, Poison)
- [ ] Mehrere Gegnertypen
- [ ] Dungeon-Level mit Wänden und Räumen
- [ ] Multiplayer (bis zu 6 Spieler)
-- [ ] Gegner-Respawn
- [ ] Boss-Gegner
+- [ ] Item-Shop / Händler
---
## Projektstruktur
```
DungeonCrawler/
-├── assets/ # 3D-Modelle (Kenney GLB)
-├── resources/
-│ ├── attack.gd # Attack-Ressourcen-Klasse
-│ └── weapon.gd # Weapon-Ressourcen-Klasse
-├── basic_attack.tres # Autoattack-Ressource
-├── camera_pivot.gd # Kamera-Script
-├── enemy.gd # Gegner-Script
-├── enemy.tscn # Gegner-Scene
-├── hud.gd # HUD-Script
-├── hud.tscn # HUD-Scene
-├── player.gd # Spieler-Script
-├── player.tscn # Spieler-Scene
-├── world.gd # Welt-Script
-├── world.tscn # Hauptszene
-└── PROJEKTDOKU.md # Diese Dokumentation
+├── 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)
+├── classes/ # Klassen-Definitionen (.tres)
+│ ├── warrior.tres # Krieger (Ressource: NONE)
+│ ├── rogue.tres # Schurke (Ressource: ENERGY, 100)
+│ └── mage.tres # Magier (Ressource: MANA, 100)
+├── consumables/ # Verbrauchbare Items (.tres)
+│ ├── small_hp_potion.tres
+│ └── small_mana_potion.tres
+├── equipment/ # Equipment-Items (.tres)
+│ ├── iron_sword.tres
+│ ├── steel_sword.tres
+│ ├── leather_chest.tres
+│ ├── iron_helm.tres
+│ └── wooden_shield.tres
+├── loot_tables/ # Loot-Tabellen (.tres)
+│ ├── goblin_loot.tres
+│ └── skeleton_loot.tres
+├── icons/ # Icons (SVG)
+│ ├── autoattack_icon.svg
+│ ├── heavy_strike_icon.svg
+│ ├── iron_sword_icon.svg
+│ ├── steel_sword_icon.svg
+│ ├── leather_chest_icon.svg
+│ ├── iron_helm_icon.svg
+│ ├── wooden_shield_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
+├── consumable.gd # Consumable Resource
+├── enemy.gd # Gegner-Script
+├── enemy.tscn # Gegner-Scene
+├── equipment.gd # Equipment Resource
+├── hud.gd # HUD-Script (inkl. ResourceBar)
+├── hud.tscn # HUD-Scene
+├── inventory.gd # Inventar Resource
+├── inventory_panel.gd # Inventar-Panel Script
+├── inventory_panel.tscn # Inventar-Panel Scene
+├── 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
+└── PROJEKTDOKU.md # Diese Dokumentation
```
diff --git a/assets/Warrior+Animation/castle_guard_01.fbx b/assets/Warrior+Animation/castle_guard_01.fbx
new file mode 100644
index 0000000..858fe3d
Binary files /dev/null and b/assets/Warrior+Animation/castle_guard_01.fbx differ
diff --git a/assets/Warrior+Animation/idle.fbx b/assets/Warrior+Animation/idle.fbx
new file mode 100644
index 0000000..ccc21ff
Binary files /dev/null and b/assets/Warrior+Animation/idle.fbx differ
diff --git a/assets/Warrior+Animation/jump.fbx b/assets/Warrior+Animation/jump.fbx
new file mode 100644
index 0000000..cf721e5
Binary files /dev/null and b/assets/Warrior+Animation/jump.fbx differ
diff --git a/assets/Warrior+Animation/left strafe walking.fbx b/assets/Warrior+Animation/left strafe walking.fbx
new file mode 100644
index 0000000..5645972
Binary files /dev/null and b/assets/Warrior+Animation/left strafe walking.fbx differ
diff --git a/assets/Warrior+Animation/left strafe.fbx b/assets/Warrior+Animation/left strafe.fbx
new file mode 100644
index 0000000..881914f
Binary files /dev/null and b/assets/Warrior+Animation/left strafe.fbx differ
diff --git a/assets/Warrior+Animation/left turn 90.fbx b/assets/Warrior+Animation/left turn 90.fbx
new file mode 100644
index 0000000..9d0a292
Binary files /dev/null and b/assets/Warrior+Animation/left turn 90.fbx differ
diff --git a/assets/Warrior+Animation/left turn.fbx b/assets/Warrior+Animation/left turn.fbx
new file mode 100644
index 0000000..cee6230
Binary files /dev/null and b/assets/Warrior+Animation/left turn.fbx differ
diff --git a/assets/Warrior+Animation/right strafe walking.fbx b/assets/Warrior+Animation/right strafe walking.fbx
new file mode 100644
index 0000000..d56012f
Binary files /dev/null and b/assets/Warrior+Animation/right strafe walking.fbx differ
diff --git a/assets/Warrior+Animation/right strafe.fbx b/assets/Warrior+Animation/right strafe.fbx
new file mode 100644
index 0000000..c533196
Binary files /dev/null and b/assets/Warrior+Animation/right strafe.fbx differ
diff --git a/assets/Warrior+Animation/right turn 90.fbx b/assets/Warrior+Animation/right turn 90.fbx
new file mode 100644
index 0000000..18fe6d4
Binary files /dev/null and b/assets/Warrior+Animation/right turn 90.fbx differ
diff --git a/assets/Warrior+Animation/right turn.fbx b/assets/Warrior+Animation/right turn.fbx
new file mode 100644
index 0000000..6c202f1
Binary files /dev/null and b/assets/Warrior+Animation/right turn.fbx differ
diff --git a/assets/Warrior+Animation/running.fbx b/assets/Warrior+Animation/running.fbx
new file mode 100644
index 0000000..9c32abe
Binary files /dev/null and b/assets/Warrior+Animation/running.fbx differ
diff --git a/assets/Warrior+Animation/walking.fbx b/assets/Warrior+Animation/walking.fbx
new file mode 100644
index 0000000..501429c
Binary files /dev/null and b/assets/Warrior+Animation/walking.fbx differ
diff --git a/assets/animations/Autoattack.fbx b/assets/animations/Autoattack.fbx
new file mode 100644
index 0000000..7ef3e54
Binary files /dev/null and b/assets/animations/Autoattack.fbx differ
diff --git a/assets/animations/Autoattack.fbx.import b/assets/animations/Autoattack.fbx.import
new file mode 100644
index 0000000..a9af76e
--- /dev/null
+++ b/assets/animations/Autoattack.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://cfmx5li2h2gsb"
+path="res://.godot/imported/Autoattack.fbx-3670ec5144a5943b8d9ee95f7186efd3.scn"
+
+[deps]
+
+source_file="res://assets/animations/Autoattack.fbx"
+dest_files=["res://.godot/imported/Autoattack.fbx-3670ec5144a5943b8d9ee95f7186efd3.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/animations/Dying Backwards.fbx b/assets/animations/Dying Backwards.fbx
new file mode 100644
index 0000000..96e97b1
Binary files /dev/null and b/assets/animations/Dying Backwards.fbx differ
diff --git a/assets/animations/Dying Backwards.fbx.import b/assets/animations/Dying Backwards.fbx.import
new file mode 100644
index 0000000..a1aeb61
--- /dev/null
+++ b/assets/animations/Dying Backwards.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://dpm8k57akvg3k"
+path="res://.godot/imported/Dying Backwards.fbx-77c7ef392c2c96235d9f4f9f7c331de8.scn"
+
+[deps]
+
+source_file="res://assets/animations/Dying Backwards.fbx"
+dest_files=["res://.godot/imported/Dying Backwards.fbx-77c7ef392c2c96235d9f4f9f7c331de8.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/animations/Heavy Strike.fbx b/assets/animations/Heavy Strike.fbx
new file mode 100644
index 0000000..bb7ffab
Binary files /dev/null and b/assets/animations/Heavy Strike.fbx differ
diff --git a/assets/animations/Heavy Strike.fbx.import b/assets/animations/Heavy Strike.fbx.import
new file mode 100644
index 0000000..0328ac1
--- /dev/null
+++ b/assets/animations/Heavy Strike.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://cmpv0gty3rcm6"
+path="res://.godot/imported/Heavy Strike.fbx-5d5103d3203a6a38edd177bdd35ccb16.scn"
+
+[deps]
+
+source_file="res://assets/animations/Heavy Strike.fbx"
+dest_files=["res://.godot/imported/Heavy Strike.fbx-5d5103d3203a6a38edd177bdd35ccb16.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/animations/Idle.fbx b/assets/animations/Idle.fbx
new file mode 100644
index 0000000..9ae9979
Binary files /dev/null and b/assets/animations/Idle.fbx differ
diff --git a/assets/animations/Idle.fbx.import b/assets/animations/Idle.fbx.import
new file mode 100644
index 0000000..105a47b
--- /dev/null
+++ b/assets/animations/Idle.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://cxr7xlsq3vvhi"
+path="res://.godot/imported/Idle.fbx-ecf8c7ec9efbd10accf3a620e1992827.scn"
+
+[deps]
+
+source_file="res://assets/animations/Idle.fbx"
+dest_files=["res://.godot/imported/Idle.fbx-ecf8c7ec9efbd10accf3a620e1992827.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/animations/Jumping.fbx b/assets/animations/Jumping.fbx
new file mode 100644
index 0000000..f690e9c
Binary files /dev/null and b/assets/animations/Jumping.fbx differ
diff --git a/assets/animations/Jumping.fbx.import b/assets/animations/Jumping.fbx.import
new file mode 100644
index 0000000..4f1163f
--- /dev/null
+++ b/assets/animations/Jumping.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://kaq2rgjrxnm4"
+path="res://.godot/imported/Jumping.fbx-785df5cde6fd6a2d61091c404e44b339.scn"
+
+[deps]
+
+source_file="res://assets/animations/Jumping.fbx"
+dest_files=["res://.godot/imported/Jumping.fbx-785df5cde6fd6a2d61091c404e44b339.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/animations/Left Strafe Walking.fbx b/assets/animations/Left Strafe Walking.fbx
new file mode 100644
index 0000000..012f3e0
Binary files /dev/null and b/assets/animations/Left Strafe Walking.fbx differ
diff --git a/assets/animations/Left Strafe Walking.fbx.import b/assets/animations/Left Strafe Walking.fbx.import
new file mode 100644
index 0000000..c604b16
--- /dev/null
+++ b/assets/animations/Left Strafe Walking.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://wurcf4qd6sn1"
+path="res://.godot/imported/Left Strafe Walking.fbx-918d1a60e568dd43e53b31199b3f57d3.scn"
+
+[deps]
+
+source_file="res://assets/animations/Left Strafe Walking.fbx"
+dest_files=["res://.godot/imported/Left Strafe Walking.fbx-918d1a60e568dd43e53b31199b3f57d3.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/animations/Right Strafe Walking.fbx b/assets/animations/Right Strafe Walking.fbx
new file mode 100644
index 0000000..12fd98f
Binary files /dev/null and b/assets/animations/Right Strafe Walking.fbx differ
diff --git a/assets/animations/Right Strafe Walking.fbx.import b/assets/animations/Right Strafe Walking.fbx.import
new file mode 100644
index 0000000..cf65000
--- /dev/null
+++ b/assets/animations/Right Strafe Walking.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://bhv5ig8xnw4v0"
+path="res://.godot/imported/Right Strafe Walking.fbx-f1318b812bb12a00a11ec8112c1f6855.scn"
+
+[deps]
+
+source_file="res://assets/animations/Right Strafe Walking.fbx"
+dest_files=["res://.godot/imported/Right Strafe Walking.fbx-f1318b812bb12a00a11ec8112c1f6855.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/animations/Start Walking.fbx b/assets/animations/Start Walking.fbx
new file mode 100644
index 0000000..ddf3269
Binary files /dev/null and b/assets/animations/Start Walking.fbx differ
diff --git a/assets/animations/Start Walking.fbx.import b/assets/animations/Start Walking.fbx.import
new file mode 100644
index 0000000..a7389b6
--- /dev/null
+++ b/assets/animations/Start Walking.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://bjwb78njpjo2d"
+path="res://.godot/imported/Start Walking.fbx-c54b281554787bf73bcae8159e84a930.scn"
+
+[deps]
+
+source_file="res://assets/animations/Start Walking.fbx"
+dest_files=["res://.godot/imported/Start Walking.fbx-c54b281554787bf73bcae8159e84a930.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/animations/Stop Walking.fbx b/assets/animations/Stop Walking.fbx
new file mode 100644
index 0000000..2e5a804
Binary files /dev/null and b/assets/animations/Stop Walking.fbx differ
diff --git a/assets/animations/Stop Walking.fbx.import b/assets/animations/Stop Walking.fbx.import
new file mode 100644
index 0000000..be1dc26
--- /dev/null
+++ b/assets/animations/Stop Walking.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://dusjhgyamhmgt"
+path="res://.godot/imported/Stop Walking.fbx-3d24a0466a0c2e2a6357ddcab01c8e7a.scn"
+
+[deps]
+
+source_file="res://assets/animations/Stop Walking.fbx"
+dest_files=["res://.godot/imported/Stop Walking.fbx-3d24a0466a0c2e2a6357ddcab01c8e7a.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/animations/Walking Backwards.fbx b/assets/animations/Walking Backwards.fbx
new file mode 100644
index 0000000..4b66973
Binary files /dev/null and b/assets/animations/Walking Backwards.fbx differ
diff --git a/assets/animations/Walking Backwards.fbx.import b/assets/animations/Walking Backwards.fbx.import
new file mode 100644
index 0000000..4a51524
--- /dev/null
+++ b/assets/animations/Walking Backwards.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://bck2nreqc2gb3"
+path="res://.godot/imported/Walking Backwards.fbx-485a35ec4e48207190c0e10717297ab7.scn"
+
+[deps]
+
+source_file="res://assets/animations/Walking Backwards.fbx"
+dest_files=["res://.godot/imported/Walking Backwards.fbx-485a35ec4e48207190c0e10717297ab7.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/animations/Walking.fbx b/assets/animations/Walking.fbx
new file mode 100644
index 0000000..28423cd
Binary files /dev/null and b/assets/animations/Walking.fbx differ
diff --git a/assets/animations/Walking.fbx.import b/assets/animations/Walking.fbx.import
new file mode 100644
index 0000000..ccf2631
--- /dev/null
+++ b/assets/animations/Walking.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://bv1wgnjxtwty1"
+path="res://.godot/imported/Walking.fbx-a17a5e9807a8458d066589326a10c768.scn"
+
+[deps]
+
+source_file="res://assets/animations/Walking.fbx"
+dest_files=["res://.godot/imported/Walking.fbx-a17a5e9807a8458d066589326a10c768.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/animations/Walking_0.png b/assets/animations/Walking_0.png
new file mode 100644
index 0000000..e90faaa
Binary files /dev/null and b/assets/animations/Walking_0.png differ
diff --git a/assets/animations/Walking_0.png.import b/assets/animations/Walking_0.png.import
new file mode 100644
index 0000000..5b39a16
--- /dev/null
+++ b/assets/animations/Walking_0.png.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://clc13cbh72eov"
+path.s3tc="res://.godot/imported/Walking_0.png-04bee5fb7233febb0f01a436248762b5.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+generator_parameters={
+"md5": "a1dc5c32f83d0182b6af19d596014cce"
+}
+
+[deps]
+
+source_file="res://assets/animations/Walking_0.png"
+dest_files=["res://.godot/imported/Walking_0.png-04bee5fb7233febb0f01a436248762b5.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/animations/Walking_1.png b/assets/animations/Walking_1.png
new file mode 100644
index 0000000..5c2868b
Binary files /dev/null and b/assets/animations/Walking_1.png differ
diff --git a/assets/animations/Walking_1.png.import b/assets/animations/Walking_1.png.import
new file mode 100644
index 0000000..056df0e
--- /dev/null
+++ b/assets/animations/Walking_1.png.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dphpf7qa3hkvh"
+path="res://.godot/imported/Walking_1.png-ba1e8e03f6c24586962fc1b62dccdfe9.ctex"
+metadata={
+"vram_texture": false
+}
+generator_parameters={
+"md5": "4201468097ebb1a15961ee25c2d64310"
+}
+
+[deps]
+
+source_file="res://assets/animations/Walking_1.png"
+dest_files=["res://.godot/imported/Walking_1.png-ba1e8e03f6c24586962fc1b62dccdfe9.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/animations/Walking_2.png b/assets/animations/Walking_2.png
new file mode 100644
index 0000000..85d82d1
Binary files /dev/null and b/assets/animations/Walking_2.png differ
diff --git a/assets/animations/Walking_2.png.import b/assets/animations/Walking_2.png.import
new file mode 100644
index 0000000..b138d32
--- /dev/null
+++ b/assets/animations/Walking_2.png.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dm2h4hkvqyoph"
+path.s3tc="res://.godot/imported/Walking_2.png-ce8a6c3320aac98a8634c6e5cf2b9141.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+generator_parameters={
+"md5": "16d2ed20714fd916290f48d68a233060"
+}
+
+[deps]
+
+source_file="res://assets/animations/Walking_2.png"
+dest_files=["res://.godot/imported/Walking_2.png-ce8a6c3320aac98a8634c6e5cf2b9141.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/animations/Walking_2.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/models/warrior.fbx b/assets/models/warrior.fbx
new file mode 100644
index 0000000..c520735
Binary files /dev/null and b/assets/models/warrior.fbx differ
diff --git a/assets/models/warrior.fbx.import b/assets/models/warrior.fbx.import
new file mode 100644
index 0000000..8ddc5b7
--- /dev/null
+++ b/assets/models/warrior.fbx.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://da1w523lg7i2b"
+path="res://.godot/imported/warrior.fbx-b4acdad06881ff9e3d6bb32ec03761c9.scn"
+
+[deps]
+
+source_file="res://assets/models/warrior.fbx"
+dest_files=["res://.godot/imported/warrior.fbx-b4acdad06881ff9e3d6bb32ec03761c9.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/models/warrior_0.png b/assets/models/warrior_0.png
new file mode 100644
index 0000000..e90faaa
Binary files /dev/null and b/assets/models/warrior_0.png differ
diff --git a/assets/models/warrior_0.png.import b/assets/models/warrior_0.png.import
new file mode 100644
index 0000000..a4caa45
--- /dev/null
+++ b/assets/models/warrior_0.png.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://kb2tcclrpsgp"
+path.s3tc="res://.godot/imported/warrior_0.png-922555626e5523f977897d33a0262592.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+generator_parameters={
+"md5": "a1dc5c32f83d0182b6af19d596014cce"
+}
+
+[deps]
+
+source_file="res://assets/models/warrior_0.png"
+dest_files=["res://.godot/imported/warrior_0.png-922555626e5523f977897d33a0262592.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/models/warrior_1.png b/assets/models/warrior_1.png
new file mode 100644
index 0000000..5c2868b
Binary files /dev/null and b/assets/models/warrior_1.png differ
diff --git a/assets/models/warrior_1.png.import b/assets/models/warrior_1.png.import
new file mode 100644
index 0000000..10537b2
--- /dev/null
+++ b/assets/models/warrior_1.png.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dnlkuf4pjgbht"
+path="res://.godot/imported/warrior_1.png-aafdc73fc8d6c19365ff4d442a16edcf.ctex"
+metadata={
+"vram_texture": false
+}
+generator_parameters={
+"md5": "4201468097ebb1a15961ee25c2d64310"
+}
+
+[deps]
+
+source_file="res://assets/models/warrior_1.png"
+dest_files=["res://.godot/imported/warrior_1.png-aafdc73fc8d6c19365ff4d442a16edcf.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/models/warrior_2.png b/assets/models/warrior_2.png
new file mode 100644
index 0000000..85d82d1
Binary files /dev/null and b/assets/models/warrior_2.png differ
diff --git a/assets/models/warrior_2.png.import b/assets/models/warrior_2.png.import
new file mode 100644
index 0000000..a3cf4ee
--- /dev/null
+++ b/assets/models/warrior_2.png.import
@@ -0,0 +1,44 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://coqdhbh5uwkjf"
+path.s3tc="res://.godot/imported/warrior_2.png-0164e54eb7a56547f2b489b55a55ca25.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+generator_parameters={
+"md5": "16d2ed20714fd916290f48d68a233060"
+}
+
+[deps]
+
+source_file="res://assets/models/warrior_2.png"
+dest_files=["res://.godot/imported/warrior_2.png-0164e54eb7a56547f2b489b55a55ca25.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/models/warrior_2.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/character_class.gd b/character_class.gd
new file mode 100644
index 0000000..19807a8
--- /dev/null
+++ b/character_class.gd
@@ -0,0 +1,37 @@
+# CharacterClass.gd
+# Definiert Charakterklassen mit Grundstats und Main-Stat
+extends Resource
+class_name CharacterClass
+
+enum MainStat { STRENGTH, AGILITY, INTELLIGENCE }
+enum ResourceType { NONE, MANA, RAGE, ENERGY }
+
+@export var class_name_de: String = "Krieger"
+@export var main_stat: MainStat = MainStat.STRENGTH
+@export var resource_type: ResourceType = ResourceType.NONE
+@export var base_resource: int = 0 # Basis-Ressource auf Level 1 (0 = keine)
+
+# Grund-Stats auf Level 1
+@export var base_strength: int = 10
+@export var base_agility: int = 10
+@export var base_intelligence: int = 10
+@export var base_stamina: int = 10 # Beeinflusst HP
+
+# Stat-Zuwachs pro Level
+@export var strength_per_level: float = 2.0
+@export var agility_per_level: float = 2.0
+@export var intelligence_per_level: float = 2.0
+@export var stamina_per_level: float = 2.0
+
+# Unbewaffneter Schaden (klassenabhängig)
+@export var unarmed_min_damage: int = 1
+@export var unarmed_max_damage: int = 2
+@export var unarmed_attack_speed: float = 2.0 # Langsamer als mit Waffe
+
+# HP pro Stamina-Punkt
+const HP_PER_STAMINA = 10
+# Mana pro Intelligenz-Punkt
+const MANA_PER_INT = 5
+
+# Schaden-Skalierung mit Main-Stat
+const DAMAGE_PER_MAIN_STAT = 0.5
diff --git a/character_class.gd.uid b/character_class.gd.uid
new file mode 100644
index 0000000..075b0d4
--- /dev/null
+++ b/character_class.gd.uid
@@ -0,0 +1 @@
+uid://ci45xxb5vn857
diff --git a/character_panel.gd b/character_panel.gd
new file mode 100644
index 0000000..f0df3eb
--- /dev/null
+++ b/character_panel.gd
@@ -0,0 +1,203 @@
+# CharacterPanel.gd
+# Zeigt Charakterinfos: Klasse, Level, Stats und Equipment-Icons mit Tooltips
+extends CanvasLayer
+
+var panel_visible = false
+
+const EQUIP_SLOT_SIZE = 48
+
+@onready var panel = $Panel
+@onready var class_label = $Panel/HBoxContainer/StatsColumn/ClassLabel
+@onready var level_label = $Panel/HBoxContainer/StatsColumn/LevelLabel
+@onready var str_label = $Panel/HBoxContainer/StatsColumn/StatsContainer/StrLabel
+@onready var agi_label = $Panel/HBoxContainer/StatsColumn/StatsContainer/AgiLabel
+@onready var int_label = $Panel/HBoxContainer/StatsColumn/StatsContainer/IntLabel
+@onready var sta_label = $Panel/HBoxContainer/StatsColumn/StatsContainer/StaLabel
+@onready var armor_label = $Panel/HBoxContainer/StatsColumn/StatsContainer/ArmorLabel
+@onready var hp_label = $Panel/HBoxContainer/StatsColumn/HPLabel
+@onready var damage_label = $Panel/HBoxContainer/EquipmentColumn/DamageLabel
+@onready var dps_label = $Panel/HBoxContainer/EquipmentColumn/DPSLabel
+@onready var equipment_grid = $Panel/HBoxContainer/EquipmentColumn/EquipmentGrid
+
+# Slot-Reihenfolge und deutsche Namen
+const SLOT_ORDER = [
+ Equipment.Slot.HEAD,
+ Equipment.Slot.CHEST,
+ Equipment.Slot.HANDS,
+ Equipment.Slot.LEGS,
+ Equipment.Slot.FEET,
+ Equipment.Slot.WEAPON,
+ Equipment.Slot.OFFHAND,
+]
+
+func _ready():
+ panel.visible = false
+
+func toggle():
+ panel_visible = !panel_visible
+ panel.visible = panel_visible
+
+func update_stats(player):
+ if player.character_class:
+ var main_stat_name = ""
+ match player.character_class.main_stat:
+ CharacterClass.MainStat.STRENGTH:
+ main_stat_name = "STR"
+ CharacterClass.MainStat.AGILITY:
+ main_stat_name = "AGI"
+ CharacterClass.MainStat.INTELLIGENCE:
+ main_stat_name = "INT"
+ class_label.text = player.character_class.class_name_de + " (Haupt: " + main_stat_name + ")"
+ else:
+ class_label.text = "Keine Klasse"
+
+ level_label.text = "Level " + str(player.level) + " (" + str(player.current_xp) + "/" + str(player.xp_to_next_level) + " XP)"
+
+ str_label.text = "Stärke: " + str(player.strength)
+ agi_label.text = "Beweglichkeit: " + str(player.agility)
+ int_label.text = "Intelligenz: " + str(player.intelligence)
+ sta_label.text = "Ausdauer: " + str(player.stamina)
+ armor_label.text = "Rüstung: " + str(player.armor)
+
+ hp_label.text = "HP: " + str(player.current_hp) + " / " + str(player.max_hp)
+
+ # Waffen-Stats
+ var weapon = player.get_equipped_weapon()
+ if weapon:
+ damage_label.text = "Schaden: " + str(weapon.min_damage) + "-" + str(weapon.max_damage) + " (%.1fs)" % weapon.attack_speed
+ else:
+ if player.character_class:
+ var min_dmg = player.character_class.unarmed_min_damage
+ var max_dmg = player.character_class.unarmed_max_damage
+ var atk_spd = player.character_class.unarmed_attack_speed
+ damage_label.text = "Unbewaffnet: " + str(min_dmg) + "-" + str(max_dmg) + " (%.1fs)" % atk_spd
+ else:
+ damage_label.text = "Unbewaffnet: 1-2 (2.0s)"
+ dps_label.text = "DPS: %.1f" % player.get_dps()
+
+ # Main-Stat hervorheben
+ str_label.modulate = Color(1, 1, 1)
+ agi_label.modulate = Color(1, 1, 1)
+ int_label.modulate = Color(1, 1, 1)
+
+ if player.character_class:
+ match player.character_class.main_stat:
+ CharacterClass.MainStat.STRENGTH:
+ str_label.modulate = Color(1, 0.8, 0.2)
+ CharacterClass.MainStat.AGILITY:
+ agi_label.modulate = Color(1, 0.8, 0.2)
+ CharacterClass.MainStat.INTELLIGENCE:
+ int_label.modulate = Color(1, 0.8, 0.2)
+
+ # Equipment-Grid mit Icons aktualisieren
+ _rebuild_equipment_grid(player)
+
+func _rebuild_equipment_grid(player):
+ # Alte Slots entfernen
+ for child in equipment_grid.get_children():
+ child.queue_free()
+
+ # Slots als Icon-Panels erstellen
+ for slot_type in SLOT_ORDER:
+ var item = player.equipment[slot_type]
+ var slot_panel = _create_equip_slot(slot_type, item)
+ equipment_grid.add_child(slot_panel)
+
+func _create_equip_slot(slot_type: Equipment.Slot, item: Equipment) -> Panel:
+ var slot = Panel.new()
+ slot.custom_minimum_size = Vector2(EQUIP_SLOT_SIZE, EQUIP_SLOT_SIZE)
+
+ # Hintergrund
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.12, 0.12, 0.12)
+ style.border_color = Color(0.3, 0.3, 0.3)
+ style.set_border_width_all(1)
+ style.set_corner_radius_all(4)
+ slot.add_theme_stylebox_override("panel", style)
+
+ if item != null:
+ # Rahmen in Seltenheitsfarbe
+ style.border_color = Equipment.get_rarity_color(item.rarity)
+ style.set_border_width_all(2)
+
+ if item.icon:
+ # Icon anzeigen
+ var icon = TextureRect.new()
+ icon.texture = item.icon
+ icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.custom_minimum_size = Vector2(EQUIP_SLOT_SIZE - 6, EQUIP_SLOT_SIZE - 6)
+ icon.position = Vector2(3, 3)
+ slot.add_child(icon)
+ else:
+ # Fallback: Kurzname
+ var label = Label.new()
+ label.text = item.item_name.substr(0, 3)
+ label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ label.add_theme_font_size_override("font_size", 10)
+ label.modulate = Equipment.get_rarity_color(item.rarity)
+ label.anchors_preset = Control.PRESET_FULL_RECT
+ slot.add_child(label)
+
+ # Tooltip mit Item-Eigenschaften
+ slot.tooltip_text = _get_item_tooltip(item)
+ else:
+ # Leerer Slot: Slot-Name als Tooltip
+ slot.tooltip_text = Equipment.get_slot_name(slot_type) + ": Leer"
+
+ # Slot-Kürzel anzeigen
+ var label = Label.new()
+ label.text = _get_slot_short(slot_type)
+ label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ label.add_theme_font_size_override("font_size", 9)
+ label.modulate = Color(0.4, 0.4, 0.4)
+ label.anchors_preset = Control.PRESET_FULL_RECT
+ slot.add_child(label)
+
+ return slot
+
+func _get_slot_short(slot_type: Equipment.Slot) -> String:
+ match slot_type:
+ Equipment.Slot.HEAD: return "Kopf"
+ Equipment.Slot.CHEST: return "Brust"
+ Equipment.Slot.HANDS: return "Hand"
+ Equipment.Slot.LEGS: return "Bein"
+ Equipment.Slot.FEET: return "Fuß"
+ Equipment.Slot.WEAPON: return "Waffe"
+ Equipment.Slot.OFFHAND: return "Neben"
+ return "?"
+
+func _get_item_tooltip(item: Equipment) -> String:
+ var tooltip = item.item_name + "\n"
+ tooltip += Equipment.get_slot_name(item.slot) + "\n"
+
+ # Seltenheit
+ match item.rarity:
+ Equipment.Rarity.COMMON: tooltip += "Gewöhnlich\n"
+ Equipment.Rarity.UNCOMMON: tooltip += "Ungewöhnlich\n"
+ Equipment.Rarity.RARE: tooltip += "Selten\n"
+ Equipment.Rarity.EPIC: tooltip += "Episch\n"
+
+ tooltip += "---\n"
+
+ if item.slot == Equipment.Slot.WEAPON:
+ tooltip += "Schaden: " + str(item.min_damage) + " - " + str(item.max_damage) + "\n"
+ tooltip += "Tempo: " + str(item.attack_speed) + "s\n"
+ tooltip += "Reichweite: " + str(item.weapon_range) + "\n"
+
+ if item.armor > 0:
+ tooltip += "Rüstung: +" + str(item.armor) + "\n"
+ if item.strength > 0:
+ tooltip += "Stärke: +" + str(item.strength) + "\n"
+ if item.agility > 0:
+ tooltip += "Beweglichkeit: +" + str(item.agility) + "\n"
+ if item.intelligence > 0:
+ tooltip += "Intelligenz: +" + str(item.intelligence) + "\n"
+ if item.stamina > 0:
+ tooltip += "Ausdauer: +" + str(item.stamina) + "\n"
+ if item.haste > 0:
+ tooltip += "Tempo: +" + str(int(item.haste * 100)) + "%\n"
+
+ return tooltip
diff --git a/character_panel.gd.uid b/character_panel.gd.uid
new file mode 100644
index 0000000..dfdb2bb
--- /dev/null
+++ b/character_panel.gd.uid
@@ -0,0 +1 @@
+uid://7pdlor66gi51
diff --git a/character_panel.tscn b/character_panel.tscn
new file mode 100644
index 0000000..ac42c7c
--- /dev/null
+++ b/character_panel.tscn
@@ -0,0 +1,124 @@
+[gd_scene format=3 uid="uid://character_panel"]
+
+[ext_resource type="Script" path="res://character_panel.gd" id="1_panel"]
+
+[node name="CharacterPanel" type="CanvasLayer"]
+script = ExtResource("1_panel")
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 20.0
+offset_top = -200.0
+offset_right = 520.0
+offset_bottom = 200.0
+grow_vertical = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 15.0
+offset_top = 15.0
+offset_right = -15.0
+offset_bottom = -15.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 20
+
+[node name="StatsColumn" type="VBoxContainer" parent="Panel/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+
+[node name="Title" type="Label" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+text = "Charakter"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+
+[node name="ClassLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Krieger (Haupt: STR)"
+
+[node name="LevelLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "Level 1 (0/100 XP)"
+
+[node name="HSeparator2" type="HSeparator" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+
+[node name="StatsContainer" type="VBoxContainer" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+theme_override_constants/separation = 4
+
+[node name="StrLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn/StatsContainer"]
+layout_mode = 2
+text = "Stärke: 15"
+
+[node name="AgiLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn/StatsContainer"]
+layout_mode = 2
+text = "Beweglichkeit: 8"
+
+[node name="IntLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn/StatsContainer"]
+layout_mode = 2
+text = "Intelligenz: 5"
+
+[node name="StaLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn/StatsContainer"]
+layout_mode = 2
+text = "Ausdauer: 12"
+
+[node name="ArmorLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn/StatsContainer"]
+layout_mode = 2
+text = "Rüstung: 0"
+
+[node name="HSeparator3" type="HSeparator" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+
+[node name="HPLabel" type="Label" parent="Panel/HBoxContainer/StatsColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "HP: 120 / 120"
+
+[node name="VSeparator" type="VSeparator" parent="Panel/HBoxContainer"]
+layout_mode = 2
+
+[node name="EquipmentColumn" type="VBoxContainer" parent="Panel/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+
+[node name="EquipTitle" type="Label" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+text = "Ausrüstung"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+
+[node name="EquipmentGrid" type="GridContainer" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+theme_override_constants/h_separation = 6
+theme_override_constants/v_separation = 6
+columns = 4
+
+[node name="HSeparator2" type="HSeparator" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+
+[node name="DamageLabel" type="Label" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "Schaden: 3-6 (1.5s)"
+
+[node name="DPSLabel" type="Label" parent="Panel/HBoxContainer/EquipmentColumn"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "DPS: 5.0"
diff --git a/class_selection_menu.gd b/class_selection_menu.gd
new file mode 100644
index 0000000..b9e6272
--- /dev/null
+++ b/class_selection_menu.gd
@@ -0,0 +1,81 @@
+# ClassSelectionMenu.gd
+# Menü zur Auswahl der Charakterklasse beim Spielstart
+extends CanvasLayer
+
+signal class_selected(character_class: CharacterClass)
+
+const WARRIOR_CLASS = preload("res://classes/warrior.tres")
+const ROGUE_CLASS = preload("res://classes/rogue.tres")
+const MAGE_CLASS = preload("res://classes/mage.tres")
+
+var selected_class: CharacterClass = null
+
+@onready var panel = $Panel
+@onready var warrior_btn = $Panel/VBoxContainer/ClassButtons/WarriorBtn
+@onready var rogue_btn = $Panel/VBoxContainer/ClassButtons/RogueBtn
+@onready var mage_btn = $Panel/VBoxContainer/ClassButtons/MageBtn
+@onready var start_btn = $Panel/VBoxContainer/StartBtn
+@onready var class_info = $Panel/VBoxContainer/ClassInfo
+@onready var stats_label = $Panel/VBoxContainer/StatsLabel
+
+func _ready():
+ # Spiel pausieren während Menü offen
+ get_tree().paused = true
+ process_mode = Node.PROCESS_MODE_ALWAYS
+
+ # Buttons verbinden
+ warrior_btn.pressed.connect(_on_warrior_selected)
+ rogue_btn.pressed.connect(_on_rogue_selected)
+ mage_btn.pressed.connect(_on_mage_selected)
+ start_btn.pressed.connect(_on_start_pressed)
+
+ # Start-Button deaktiviert bis Klasse gewählt
+ start_btn.disabled = true
+ class_info.text = "Wähle eine Klasse!"
+ stats_label.text = ""
+
+func _on_warrior_selected():
+ selected_class = WARRIOR_CLASS
+ _update_selection("Krieger", WARRIOR_CLASS)
+ _highlight_button(warrior_btn)
+
+func _on_rogue_selected():
+ selected_class = ROGUE_CLASS
+ _update_selection("Schurke", ROGUE_CLASS)
+ _highlight_button(rogue_btn)
+
+func _on_mage_selected():
+ selected_class = MAGE_CLASS
+ _update_selection("Magier", MAGE_CLASS)
+ _highlight_button(mage_btn)
+
+func _update_selection(cls_name: String, char_class: CharacterClass):
+ var main_stat_name = ""
+ match char_class.main_stat:
+ CharacterClass.MainStat.STRENGTH:
+ main_stat_name = "Stärke"
+ CharacterClass.MainStat.AGILITY:
+ main_stat_name = "Beweglichkeit"
+ CharacterClass.MainStat.INTELLIGENCE:
+ main_stat_name = "Intelligenz"
+
+ class_info.text = cls_name + " - Haupt-Stat: " + main_stat_name
+ stats_label.text = "STR: " + str(char_class.base_strength) + " AGI: " + str(char_class.base_agility) + " INT: " + str(char_class.base_intelligence) + " STA: " + str(char_class.base_stamina)
+ start_btn.disabled = false
+
+func _highlight_button(active_btn: Button):
+ # Alle Buttons zurücksetzen
+ warrior_btn.modulate = Color(1, 1, 1)
+ rogue_btn.modulate = Color(1, 1, 1)
+ mage_btn.modulate = Color(1, 1, 1)
+ # Aktiven Button hervorheben
+ active_btn.modulate = Color(1, 0.8, 0.2)
+
+func _on_start_pressed():
+ if selected_class == null:
+ return
+
+ # Signal senden und Menü schließen
+ class_selected.emit(selected_class)
+ get_tree().paused = false
+ queue_free()
diff --git a/class_selection_menu.gd.uid b/class_selection_menu.gd.uid
new file mode 100644
index 0000000..493bc1f
--- /dev/null
+++ b/class_selection_menu.gd.uid
@@ -0,0 +1 @@
+uid://db8m2uw42hqfc
diff --git a/class_selection_menu.tscn b/class_selection_menu.tscn
new file mode 100644
index 0000000..337d396
--- /dev/null
+++ b/class_selection_menu.tscn
@@ -0,0 +1,80 @@
+[gd_scene format=3 uid="uid://class_selection_menu"]
+
+[ext_resource type="Script" path="res://class_selection_menu.gd" id="1_menu"]
+
+[node name="ClassSelectionMenu" type="CanvasLayer"]
+script = ExtResource("1_menu")
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -180.0
+offset_right = 200.0
+offset_bottom = 180.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 20.0
+offset_top = 20.0
+offset_right = -20.0
+offset_bottom = -20.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 15
+
+[node name="Title" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 28
+text = "Wähle deine Klasse"
+horizontal_alignment = 1
+
+[node name="ClassButtons" type="HBoxContainer" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 20
+alignment = 1
+
+[node name="WarriorBtn" type="Button" parent="Panel/VBoxContainer/ClassButtons"]
+custom_minimum_size = Vector2(100, 80)
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Krieger"
+
+[node name="RogueBtn" type="Button" parent="Panel/VBoxContainer/ClassButtons"]
+custom_minimum_size = Vector2(100, 80)
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Schurke"
+
+[node name="MageBtn" type="Button" parent="Panel/VBoxContainer/ClassButtons"]
+custom_minimum_size = Vector2(100, 80)
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Magier"
+
+[node name="ClassInfo" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 18
+text = "Wähle eine Klasse!"
+horizontal_alignment = 1
+
+[node name="StatsLabel" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = ""
+horizontal_alignment = 1
+
+[node name="StartBtn" type="Button" parent="Panel/VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+disabled = true
+text = "Spiel starten"
diff --git a/classes/mage.tres b/classes/mage.tres
new file mode 100644
index 0000000..df179f8
--- /dev/null
+++ b/classes/mage.tres
@@ -0,0 +1,21 @@
+[gd_resource type="Resource" script_class="CharacterClass" format=3 uid="uid://24g7k0k7vbr6"]
+
+[ext_resource type="Script" uid="uid://ci45xxb5vn857" path="res://character_class.gd" id="1"]
+
+[resource]
+script = ExtResource("1")
+class_name_de = "Magier"
+main_stat = 2
+resource_type = 1
+base_resource = 100
+base_strength = 5
+base_agility = 8
+base_intelligence = 15
+base_stamina = 8
+strength_per_level = 1.0
+agility_per_level = 1.5
+intelligence_per_level = 3.0
+stamina_per_level = 1.5
+unarmed_min_damage = 2
+unarmed_max_damage = 5
+unarmed_attack_speed = 2.0
diff --git a/classes/rogue.tres b/classes/rogue.tres
new file mode 100644
index 0000000..2d01969
--- /dev/null
+++ b/classes/rogue.tres
@@ -0,0 +1,18 @@
+[gd_resource type="Resource" script_class="CharacterClass" format=3 uid="uid://cdcx81iw6hxic"]
+
+[ext_resource type="Script" uid="uid://ci45xxb5vn857" path="res://character_class.gd" id="1"]
+
+[resource]
+script = ExtResource("1")
+class_name_de = "Schurke"
+main_stat = 1
+resource_type = 3
+base_resource = 100
+base_strength = 8
+base_agility = 15
+base_intelligence = 7
+strength_per_level = 1.5
+agility_per_level = 3.0
+intelligence_per_level = 1.5
+unarmed_max_damage = 3
+unarmed_attack_speed = 1.5
diff --git a/classes/warrior.tres b/classes/warrior.tres
new file mode 100644
index 0000000..246b0b2
--- /dev/null
+++ b/classes/warrior.tres
@@ -0,0 +1,17 @@
+[gd_resource type="Resource" script_class="CharacterClass" format=3 uid="uid://bebfnwygqy1gu"]
+
+[ext_resource type="Script" uid="uid://ci45xxb5vn857" path="res://character_class.gd" id="1"]
+
+[resource]
+script = ExtResource("1")
+base_strength = 15
+base_agility = 8
+base_intelligence = 5
+base_stamina = 12
+strength_per_level = 3.0
+agility_per_level = 1.5
+intelligence_per_level = 1.0
+stamina_per_level = 2.5
+unarmed_min_damage = 2
+unarmed_max_damage = 4
+unarmed_attack_speed = 1.8
diff --git a/consumable.gd b/consumable.gd
new file mode 100644
index 0000000..79b5579
--- /dev/null
+++ b/consumable.gd
@@ -0,0 +1,55 @@
+# Consumable.gd
+# Resource für verbrauchbare Items (Tränke, Essen, etc.)
+extends Resource
+class_name Consumable
+
+enum EffectType {
+ HEAL_HP, # HP heilen
+ RESTORE_MANA, # Mana wiederherstellen
+ HEAL_AND_MANA, # Beides
+}
+
+@export var item_name: String = "Trank"
+@export var effect_type: EffectType = EffectType.HEAL_HP
+@export var amount: int = 50 # Wie viel geheilt/wiederhergestellt wird
+@export var cooldown: float = 1.0 # Cooldown nach Benutzung
+@export var icon: Texture2D
+@export var stack_size: int = 1 # Aktuelle Anzahl im Stack
+@export var max_stack: int = 20 # Maximale Stack-Größe
+
+# Effekt-Beschreibung für Tooltip
+func get_effect_text() -> String:
+ match effect_type:
+ EffectType.HEAL_HP:
+ return "Stellt " + str(amount) + " HP wieder her"
+ EffectType.RESTORE_MANA:
+ return "Stellt " + str(amount) + " Mana wieder her"
+ EffectType.HEAL_AND_MANA:
+ return "Stellt " + str(amount) + " HP und Mana wieder her"
+ return ""
+
+# Effekt auf Spieler anwenden, gibt true zurück wenn verbraucht
+func use(player) -> bool:
+ match effect_type:
+ EffectType.HEAL_HP:
+ if player.current_hp >= player.max_hp:
+ print("HP bereits voll!")
+ return false
+ player.heal(amount)
+ print("+" + str(amount) + " HP")
+ EffectType.RESTORE_MANA:
+ if player.current_mana >= player.max_mana:
+ print("Mana bereits voll!")
+ return false
+ player.restore_mana(amount)
+ print("+" + str(amount) + " Mana")
+ EffectType.HEAL_AND_MANA:
+ if player.current_hp >= player.max_hp and player.current_mana >= player.max_mana:
+ print("HP und Mana bereits voll!")
+ return false
+ player.heal(amount)
+ player.restore_mana(amount)
+ print("+" + str(amount) + " HP und Mana")
+
+ stack_size -= 1
+ return true
diff --git a/consumable.gd.uid b/consumable.gd.uid
new file mode 100644
index 0000000..c31ee27
--- /dev/null
+++ b/consumable.gd.uid
@@ -0,0 +1 @@
+uid://b25cvd8swhsg3
diff --git a/consumables/small_hp_potion.tres b/consumables/small_hp_potion.tres
new file mode 100644
index 0000000..7708eb1
--- /dev/null
+++ b/consumables/small_hp_potion.tres
@@ -0,0 +1,14 @@
+[gd_resource type="Resource" script_class="Consumable" load_steps=3 format=3]
+
+[ext_resource type="Script" path="res://consumable.gd" id="1"]
+[ext_resource type="Texture2D" path="res://icons/hp_potion_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1")
+item_name = "Kleiner Heiltrank"
+effect_type = 0
+amount = 50
+cooldown = 1.0
+icon = ExtResource("2_icon")
+stack_size = 5
+max_stack = 20
diff --git a/consumables/small_mana_potion.tres b/consumables/small_mana_potion.tres
new file mode 100644
index 0000000..d838cba
--- /dev/null
+++ b/consumables/small_mana_potion.tres
@@ -0,0 +1,14 @@
+[gd_resource type="Resource" script_class="Consumable" load_steps=3 format=3]
+
+[ext_resource type="Script" path="res://consumable.gd" id="1"]
+[ext_resource type="Texture2D" path="res://icons/mana_potion_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1")
+item_name = "Kleiner Manatrank"
+effect_type = 1
+amount = 40
+cooldown = 1.0
+icon = ExtResource("2_icon")
+stack_size = 5
+max_stack = 20
diff --git a/enemy.gd b/enemy.gd
index 7a60e6b..72f9a5c 100644
--- a/enemy.gd
+++ b/enemy.gd
@@ -2,26 +2,166 @@
# Steuert den Gegner: KI-Bewegung zum Spieler, Angriff, HP, Zielanzeige
extends CharacterBody3D
+signal enemy_died(spawn_position: Vector3, xp_reward: int)
+signal enemy_dropped_loot(loot: Dictionary, world_position: Vector3)
+
const SPEED = 3.0
+const PATROL_SPEED = 1.5
const GRAVITY = 9.8
-const ATTACK_DAMAGE = 5
const ATTACK_RANGE = 1.5
const ATTACK_COOLDOWN = 2.0
+const AGGRO_RANGE = 8.0 # Entfernung ab der der Gegner angreift
+const PATROL_RADIUS = 5.0 # Radius um Spawn-Position für Patrol
+const PATROL_WAIT_TIME = 2.0 # Wartezeit am Patrol-Punkt
-var max_hp = 50
-var current_hp = 50
-var target = null # Ziel des Gegners (normalerweise der Spieler)
+# Level-Differenz Konstanten
+const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz
+const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation
+
+enum State { PATROL, CHASE, ATTACK }
+
+# Stats-System
+@export var level: int = 1
+@export var base_strength: int = 8
+@export var base_stamina: int = 10
+@export var base_armor: int = 5 # Rüstung reduziert Nahkampfschaden
+
+# Berechnete Stats
+var strength: int = 8
+var stamina: int = 10
+var armor: int = 5
+var max_hp: int = 100
+var current_hp: int = 100
+var attack_damage: int = 5
+
+# XP-Belohnung (skaliert mit Level)
+var xp_reward: int = 25
+
+# Loot-System
+@export var loot_table: LootTable
+
+var target = null # Spieler-Referenz (wird von World gesetzt)
var can_attack = true
+var spawn_position: Vector3 # Ursprüngliche Spawn-Position
+var current_state = State.PATROL
+var patrol_target: Vector3 # Aktuelles Patrol-Ziel
+var is_waiting = false # Ob Gegner am Patrol-Punkt wartet
+
+# Animation System
+const ANIMATION_FILES = {
+ "walk": "res://assets/animations/Walking.fbx",
+ "autoattack": "res://assets/animations/Autoattack.fbx",
+ "die": "res://assets/animations/Dying Backwards.fbx",
+ "idle": "res://assets/animations/Idle.fbx",
+}
+const LOOP_ANIMATIONS = ["walk", "idle"]
+var anim_player: AnimationPlayer = null
+var current_anim: String = ""
@onready var health_label = $HealthLabel
func _ready():
+ _calculate_stats()
+ current_hp = max_hp
health_label.visible = false
_update_label()
+ spawn_position = global_position
+ _pick_new_patrol_target()
+ _setup_animations()
+
+# Animationen laden
+func _setup_animations():
+ var model = get_node_or_null("EnemyModel")
+ if model == null:
+ return
+ anim_player = _find_node_by_class(model, "AnimationPlayer")
+ if anim_player == null:
+ anim_player = AnimationPlayer.new()
+ anim_player.name = "AnimationPlayer"
+ model.add_child(anim_player)
+ var lib: AnimationLibrary
+ if anim_player.has_animation_library(""):
+ lib = anim_player.get_animation_library("")
+ else:
+ lib = AnimationLibrary.new()
+ anim_player.add_animation_library("", lib)
+ for anim_id in ANIMATION_FILES:
+ var scene = load(ANIMATION_FILES[anim_id]) as PackedScene
+ if scene == null:
+ continue
+ var instance = scene.instantiate()
+ var source_ap = _find_node_by_class(instance, "AnimationPlayer")
+ if source_ap:
+ var names = source_ap.get_animation_list()
+ if names.size() > 0:
+ var anim = source_ap.get_animation(names[0])
+ if anim_id in ["walk", "idle"]:
+ anim.loop_mode = Animation.LOOP_LINEAR
+ if lib.has_animation(anim_id):
+ lib.remove_animation(anim_id)
+ lib.add_animation(anim_id, anim)
+ instance.queue_free()
+
+func _find_node_by_class(node: Node, class_name_str: String) -> Node:
+ for child in node.get_children():
+ if child.get_class() == class_name_str:
+ return child
+ var result = _find_node_by_class(child, class_name_str)
+ if result:
+ return result
+ return null
+
+func _play_anim(anim_name: String):
+ if anim_player == null:
+ return
+ # Wenn Animation ausgelaufen ist, zurücksetzen
+ if not anim_player.is_playing():
+ current_anim = ""
+ if anim_name != current_anim:
+ current_anim = anim_name
+ if anim_player.has_animation(anim_name):
+ anim_player.play(anim_name)
+ else:
+ anim_player.stop()
+
+# Stats basierend auf Level berechnen
+func _calculate_stats():
+ var levels_gained = level - 1
+ strength = base_strength + levels_gained * 2
+ stamina = base_stamina + levels_gained * 3
+ armor = base_armor + levels_gained * 2
+
+ # HP = Stamina * 10
+ max_hp = stamina * 10
+ # Schaden = Stärke / 2
+ attack_damage = int(strength * 0.5) + 2
+ # XP = 25 * Level
+ xp_reward = 25 * level
+
+ print("Enemy Stats (Lv", level, ") - STR:", strength, " STA:", stamina, " ARM:", armor, " HP:", max_hp, " DMG:", attack_damage)
+
+# Schaden mit Rüstung und Level-Differenz berechnen
+func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int:
+ var damage = float(raw_damage)
+
+ # Rüstung reduziert nur Nahkampfschaden
+ if is_melee:
+ # Rüstungsreduktion: armor / (armor + 50) = Prozent Reduktion
+ # Bei 5 Rüstung: 5/55 = ~9% Reduktion
+ # Bei 20 Rüstung: 20/70 = ~29% Reduktion
+ var armor_reduction = float(armor) / (float(armor) + 50.0)
+ damage = damage * (1.0 - armor_reduction)
+
+ # Level-Differenz Modifikator
+ var level_diff = attacker_level - level
+ var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD)
+ damage = damage * (1.0 + level_mod)
+
+ return maxi(1, int(damage)) # Mindestens 1 Schaden
# HP-Label Text aktualisieren
func _update_label():
- health_label.text = str(current_hp) + " / " + str(max_hp)
+ health_label.text = "Lv" + str(level) + " " + str(current_hp) + "/" + str(max_hp)
# HP-Label anzeigen (wenn Gegner markiert wird)
func show_health():
@@ -35,45 +175,145 @@ func hide_health():
func take_damage(amount):
current_hp -= amount
_update_label()
+ # Aggro bei Schaden — sofort angreifen
+ if current_state == State.PATROL:
+ current_state = State.CHASE
+ is_waiting = false
+ print("Gegner wurde angegriffen und verfolgt den Spieler!")
if current_hp <= 0:
die()
+# Schaden mit vollem Schadenssystem (Rüstung, Level-Differenz)
+func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true):
+ var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee)
+ print("Eingehender Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)")
+ take_damage(final_damage)
+
# Gegner aus der Szene entfernen
func die():
- print("Gegner besiegt!")
+ print("Gegner besiegt! +", xp_reward, " XP")
+ # XP an Spieler geben
+ if target and target.has_method("gain_xp"):
+ target.gain_xp(xp_reward)
+ # Loot generieren und droppen
+ _drop_loot()
+ enemy_died.emit(spawn_position, xp_reward)
+ # Death-Animation abspielen, dann entfernen
+ if anim_player and anim_player.has_animation("die"):
+ _play_anim("die")
+ # Kollision deaktivieren damit der Gegner nicht mehr im Weg ist
+ set_physics_process(false)
+ $CollisionShape3D.set_deferred("disabled", true)
+ await anim_player.animation_finished
queue_free()
+# Loot generieren basierend auf LootTable
+func _drop_loot():
+ if loot_table == null:
+ # Standard-Gold-Drop wenn keine LootTable zugewiesen
+ var gold = randi_range(1, 3) * level
+ var loot = {"gold": gold, "items": []}
+ enemy_dropped_loot.emit(loot, global_position)
+ return
+
+ var loot = loot_table.generate_loot()
+ # Gold mit Level skalieren
+ loot["gold"] = loot["gold"] * level
+ enemy_dropped_loot.emit(loot, global_position)
+
func _physics_process(delta):
if not is_on_floor():
velocity.y -= GRAVITY * delta
if target == null:
+ # Ohne Spieler-Referenz nur patrouillieren
+ _do_patrol()
move_and_slide()
return
- var distance = global_position.distance_to(target.global_position)
+ # Prüfe Distanz zum Spieler für Aggro
+ var distance_to_player = global_position.distance_to(target.global_position)
- if distance <= ATTACK_RANGE:
- # In Reichweite: angreifen
- velocity.x = 0
- velocity.z = 0
- if can_attack:
- _attack()
- else:
- # Direkt auf Ziel zubewegen
- var direction = (target.global_position - global_position)
- direction.y = 0
- direction = direction.normalized()
- velocity.x = direction.x * SPEED
- velocity.z = direction.z * SPEED
- look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z))
+ # State-Wechsel basierend auf Distanz
+ match current_state:
+ State.PATROL:
+ if distance_to_player <= AGGRO_RANGE:
+ current_state = State.CHASE
+ print("Gegner hat Spieler entdeckt!")
+ else:
+ _do_patrol()
+ State.CHASE:
+ if distance_to_player <= ATTACK_RANGE:
+ current_state = State.ATTACK
+ else:
+ _chase_player()
+ State.ATTACK:
+ if distance_to_player > ATTACK_RANGE:
+ current_state = State.CHASE
+ else:
+ velocity.x = 0
+ velocity.z = 0
+ _play_anim("idle") # Idle beim Angriff
+ if can_attack:
+ _attack()
move_and_slide()
+# Neues Patrol-Ziel in der Nähe der Spawn-Position wählen
+func _pick_new_patrol_target():
+ var angle = randf() * TAU # Zufälliger Winkel
+ var distance = randf_range(2.0, PATROL_RADIUS)
+ patrol_target = spawn_position + Vector3(cos(angle) * distance, 0, sin(angle) * distance)
+
+# Patrol-Verhalten: Zufällig herumlaufen
+func _do_patrol():
+ if is_waiting:
+ return
+
+ var distance_to_patrol = global_position.distance_to(patrol_target)
+
+ if distance_to_patrol <= 0.5:
+ # Am Ziel angekommen, warten und neues Ziel wählen
+ velocity.x = 0
+ velocity.z = 0
+ _play_anim("idle")
+ _wait_at_patrol_point()
+ else:
+ # Zum Patrol-Ziel laufen
+ var direction = (patrol_target - global_position)
+ direction.y = 0
+ direction = direction.normalized()
+ velocity.x = direction.x * PATROL_SPEED
+ velocity.z = direction.z * PATROL_SPEED
+ _play_anim("walk")
+ look_at(Vector3(patrol_target.x, global_position.y, patrol_target.z))
+
+# Am Patrol-Punkt warten
+func _wait_at_patrol_point():
+ is_waiting = true
+ await get_tree().create_timer(PATROL_WAIT_TIME).timeout
+ is_waiting = false
+ _pick_new_patrol_target()
+
+# Spieler verfolgen
+func _chase_player():
+ _play_anim("walk")
+ var direction = (target.global_position - global_position)
+ direction.y = 0
+ direction = direction.normalized()
+ velocity.x = direction.x * SPEED
+ velocity.z = direction.z * SPEED
+ look_at(Vector3(target.global_position.x, global_position.y, target.global_position.z))
+
# Angriff mit Cooldown
func _attack():
can_attack = false
- target.take_damage(ATTACK_DAMAGE)
- print("Gegner greift an!")
+ _play_anim("autoattack")
+ # Gegner verwendet auch das Schadenssystem mit Level-Differenz
+ if target.has_method("take_damage_from"):
+ target.take_damage_from(attack_damage, level, true)
+ else:
+ target.take_damage(attack_damage)
+ print("Gegner (Lv", level, ") greift an: ", attack_damage, " Schaden")
await get_tree().create_timer(ATTACK_COOLDOWN).timeout
can_attack = true
diff --git a/enemy.tscn b/enemy.tscn
index 823a15e..cb51e5a 100644
--- a/enemy.tscn
+++ b/enemy.tscn
@@ -1,6 +1,7 @@
[gd_scene format=3 uid="uid://cvojaeanxugfj"]
-[ext_resource type="PackedScene" uid="uid://tosl2au4emxt" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-d.glb" id="1_7k104"]
+[ext_resource type="PackedScene" uid="uid://da1w523lg7i2b" path="res://assets/models/warrior.fbx" id="1_7k104"]
+[ext_resource type="Script" uid="uid://bg5qs3pcfp7p7" path="res://enemy.gd" id="2_enemy"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4gyqm"]
radius = 0.6
@@ -11,8 +12,9 @@ radius = 0.6
height = 3.0
[node name="Enemy" type="CharacterBody3D" unique_id=332011146]
+script = ExtResource("2_enemy")
-[node name="character-d2" parent="." unique_id=846574684 instance=ExtResource("1_7k104")]
+[node name="EnemyModel" parent="." unique_id=846574684 instance=ExtResource("1_7k104")]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1323028920]
diff --git a/equipment.gd b/equipment.gd
new file mode 100644
index 0000000..bc40061
--- /dev/null
+++ b/equipment.gd
@@ -0,0 +1,63 @@
+# Equipment.gd
+# Resource für Ausrüstungsgegenstände
+extends Resource
+class_name Equipment
+
+enum Slot {
+ HEAD, # Helm
+ CHEST, # Brustpanzer
+ HANDS, # Handschuhe
+ LEGS, # Beinschienen
+ FEET, # Stiefel
+ WEAPON, # Waffe
+ OFFHAND # Nebenhand (Schild, etc.)
+}
+
+enum Rarity {
+ COMMON, # Weiß
+ UNCOMMON, # Grün
+ RARE, # Blau
+ EPIC # Lila
+}
+
+@export var item_name: String = "Unbekannt"
+@export var slot: Slot = Slot.WEAPON
+@export var rarity: Rarity = Rarity.COMMON
+
+# Stats die das Item gibt
+@export var armor: int = 0
+@export var strength: int = 0
+@export var agility: int = 0
+@export var intelligence: int = 0
+@export var stamina: int = 0
+@export var haste: float = 0.0 # Angriffsgeschwindigkeit (0.1 = 10% schneller)
+
+# Nur für Waffen
+@export var min_damage: int = 0
+@export var max_damage: int = 0
+@export var attack_speed: float = 1.5 # Sekunden zwischen Angriffen
+@export var weapon_range: float = 3.0
+
+# Icon für UI
+@export var icon: Texture2D
+
+# Slot-Namen für Anzeige
+static func get_slot_name(s: Slot) -> String:
+ match s:
+ Slot.HEAD: return "Kopf"
+ Slot.CHEST: return "Brust"
+ Slot.HANDS: return "Hände"
+ Slot.LEGS: return "Beine"
+ Slot.FEET: return "Füße"
+ Slot.WEAPON: return "Waffe"
+ Slot.OFFHAND: return "Nebenhand"
+ return "Unbekannt"
+
+# Seltenheitsfarbe
+static func get_rarity_color(r: Rarity) -> Color:
+ match r:
+ Rarity.COMMON: return Color(1, 1, 1) # Weiß
+ Rarity.UNCOMMON: return Color(0.2, 0.8, 0.2) # Grün
+ Rarity.RARE: return Color(0.3, 0.5, 1.0) # Blau
+ Rarity.EPIC: return Color(0.7, 0.3, 0.9) # Lila
+ return Color(1, 1, 1)
diff --git a/equipment.gd.uid b/equipment.gd.uid
new file mode 100644
index 0000000..361aeaf
--- /dev/null
+++ b/equipment.gd.uid
@@ -0,0 +1 @@
+uid://re0xiie1udfq
diff --git a/equipment/iron_helm.tres b/equipment/iron_helm.tres
new file mode 100644
index 0000000..bd28010
--- /dev/null
+++ b/equipment/iron_helm.tres
@@ -0,0 +1,13 @@
+[gd_resource type="Resource" script_class="Equipment" format=3 uid="uid://d54jjc24pj3t"]
+
+[ext_resource type="Script" uid="uid://re0xiie1udfq" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" uid="uid://c8x8d055wjcpu" path="res://icons/iron_helm_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Eisenhelm"
+slot = 0
+armor = 5
+strength = 1
+stamina = 1
+icon = ExtResource("2_icon")
diff --git a/equipment/iron_sword.tres b/equipment/iron_sword.tres
new file mode 100644
index 0000000..9557429
--- /dev/null
+++ b/equipment/iron_sword.tres
@@ -0,0 +1,12 @@
+[gd_resource type="Resource" script_class="Equipment" format=3 uid="uid://vprfv2phlcbc"]
+
+[ext_resource type="Script" uid="uid://re0xiie1udfq" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" uid="uid://bi7sh66txlj1c" path="res://icons/iron_sword_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Eisenschwert"
+strength = 2
+min_damage = 3
+max_damage = 6
+icon = ExtResource("2_icon")
diff --git a/equipment/leather_chest.tres b/equipment/leather_chest.tres
new file mode 100644
index 0000000..865a2e1
--- /dev/null
+++ b/equipment/leather_chest.tres
@@ -0,0 +1,13 @@
+[gd_resource type="Resource" script_class="Equipment" format=3 uid="uid://cnijhxsjmepss"]
+
+[ext_resource type="Script" uid="uid://re0xiie1udfq" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" uid="uid://c6kmscwfv0ssa" path="res://icons/leather_chest_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Lederrüstung"
+slot = 1
+armor = 8
+agility = 1
+stamina = 2
+icon = ExtResource("2_icon")
diff --git a/equipment/steel_sword.tres b/equipment/steel_sword.tres
new file mode 100644
index 0000000..a10502b
--- /dev/null
+++ b/equipment/steel_sword.tres
@@ -0,0 +1,20 @@
+[gd_resource type="Resource" script_class="Equipment" load_steps=3 format=3]
+
+[ext_resource type="Script" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" path="res://icons/steel_sword_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Stahlschwert"
+slot = 5
+rarity = 1
+armor = 0
+strength = 4
+agility = 0
+intelligence = 0
+stamina = 0
+min_damage = 5
+max_damage = 9
+attack_speed = 1.4
+weapon_range = 3.0
+icon = ExtResource("2_icon")
diff --git a/equipment/wooden_shield.tres b/equipment/wooden_shield.tres
new file mode 100644
index 0000000..8fcc184
--- /dev/null
+++ b/equipment/wooden_shield.tres
@@ -0,0 +1,20 @@
+[gd_resource type="Resource" script_class="Equipment" load_steps=3 format=3]
+
+[ext_resource type="Script" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" path="res://icons/wooden_shield_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Holzschild"
+slot = 6
+rarity = 0
+armor = 6
+strength = 0
+agility = 0
+intelligence = 0
+stamina = 2
+min_damage = 0
+max_damage = 0
+attack_speed = 1.5
+weapon_range = 3.0
+icon = ExtResource("2_icon")
diff --git a/equipment/wooden_staff.tres b/equipment/wooden_staff.tres
new file mode 100644
index 0000000..d099b83
--- /dev/null
+++ b/equipment/wooden_staff.tres
@@ -0,0 +1,14 @@
+[gd_resource type="Resource" script_class="Equipment" format=3 uid="uid://wooden_staff_01"]
+
+[ext_resource type="Script" uid="uid://re0xiie1udfq" path="res://equipment.gd" id="1_equipment"]
+[ext_resource type="Texture2D" path="res://icons/wooden_staff_icon.svg" id="2_icon"]
+
+[resource]
+script = ExtResource("1_equipment")
+item_name = "Holzstab"
+intelligence = 3
+min_damage = 2
+max_damage = 5
+attack_speed = 2.0
+weapon_range = 3.0
+icon = ExtResource("2_icon")
diff --git a/heavy_strike.tres b/heavy_strike.tres
new file mode 100644
index 0000000..317d522
--- /dev/null
+++ b/heavy_strike.tres
@@ -0,0 +1,9 @@
+[gd_resource type="Resource" script_class="Attack" load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://resources/attack.gd" id="1"]
+
+[resource]
+script = ExtResource("1")
+name = "Heavy Strike"
+damage_type = 0
+icon = null
diff --git a/hud.gd b/hud.gd
index 32798cb..f0b0ac4 100644
--- a/hud.gd
+++ b/hud.gd
@@ -1,9 +1,25 @@
# HUD.gd
-# Verwaltet die Spieler-UI: HP-Leiste, Aktionsleiste (Slots 1-9)
+# Verwaltet die Spieler-UI: HP-Leiste, XP-Leiste, Aktionsleiste (Slots 1-9)
extends CanvasLayer
+signal slot_clicked(slot_index: int)
+signal slot_drag_removed(slot_index: int)
+signal slot_drag_swapped(from_slot: int, to_slot: int)
+
+# Drag & Drop State
+var drag_active = false
+var drag_highlight_slot = -1
+var drag_from_slot = -1 # Slot von dem aus gedraggt wird
+var drag_icon: TextureRect = null
+var drag_item = null # Das gedraggte Consumable
+
@onready var health_bar = $Control/HealthBar
@onready var health_label = $Control/HealthBar/HealthLabel
+
+# Level/XP UI (wird dynamisch erstellt)
+var level_label: Label
+var xp_bar: ProgressBar
+var gold_label: Label
@onready var action_slots = [
$Control/ActionBar/A1,
$Control/ActionBar/A2,
@@ -17,6 +33,281 @@ extends CanvasLayer
]
var active_slot = 0
+var slot_icons = [] # TextureRect nodes für Icons
+var slot_cooldown_overlays = [] # ColorRect für Cooldown-Anzeige
+var slot_cooldown_labels = [] # Label für Cooldown-Text
+var slot_stack_labels = [] # Label für Stack-Anzahl
+
+# Ressourcen-Bar (Mana/Energie/Wut)
+var resource_bar: ProgressBar
+var resource_label: Label
+
+# Castbar
+var castbar: ProgressBar
+var castbar_label: Label
+var castbar_container: PanelContainer
+
+func _ready():
+ _create_level_ui()
+
+ for i in range(9):
+ # Icon erstellen
+ var icon = TextureRect.new()
+ icon.name = "Icon"
+ icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.custom_minimum_size = Vector2(40, 40)
+ icon.position = Vector2(5, 5)
+ action_slots[i].add_child(icon)
+ slot_icons.append(icon)
+
+ # Cooldown-Overlay erstellen (dunkle Überlagerung)
+ var cooldown_overlay = ColorRect.new()
+ cooldown_overlay.name = "CooldownOverlay"
+ cooldown_overlay.color = Color(0, 0, 0, 0.7)
+ cooldown_overlay.size = Vector2(50, 50)
+ cooldown_overlay.position = Vector2(0, 0)
+ cooldown_overlay.visible = false
+ cooldown_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ action_slots[i].add_child(cooldown_overlay)
+ slot_cooldown_overlays.append(cooldown_overlay)
+
+ # Cooldown-Text erstellen
+ var cooldown_label = Label.new()
+ cooldown_label.name = "CooldownLabel"
+ cooldown_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ cooldown_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ cooldown_label.size = Vector2(50, 50)
+ cooldown_label.position = Vector2(0, 0)
+ cooldown_label.add_theme_font_size_override("font_size", 16)
+ cooldown_label.visible = false
+ cooldown_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ action_slots[i].add_child(cooldown_label)
+ slot_cooldown_labels.append(cooldown_label)
+
+ # Stack-Count Label (unten rechts)
+ var stack_label = Label.new()
+ stack_label.name = "StackLabel"
+ stack_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ stack_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
+ stack_label.size = Vector2(50, 50)
+ stack_label.position = Vector2(-4, -2)
+ stack_label.add_theme_font_size_override("font_size", 11)
+ stack_label.visible = false
+ stack_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ action_slots[i].add_child(stack_label)
+ slot_stack_labels.append(stack_label)
+
+ # Button für Klicks und Drag erstellen
+ var button = Button.new()
+ button.name = "SlotButton"
+ button.flat = true
+ button.size = Vector2(50, 50)
+ button.position = Vector2(0, 0)
+ button.modulate = Color(1, 1, 1, 0) # Unsichtbar
+ var slot_index = i
+ button.pressed.connect(func(): _on_slot_clicked(slot_index))
+ button.button_down.connect(func(): _on_slot_drag_start(slot_index))
+ action_slots[i].add_child(button)
+
+# Drag aus Aktionsleiste starten
+func _on_slot_drag_start(slot_index: int):
+ # Prüfe ob ein Consumable im Slot liegt (wird vom Player gesetzt)
+ # Signal an Player senden um Item abzufragen
+ _start_actionbar_drag(slot_index)
+
+func _start_actionbar_drag(slot_index: int):
+ drag_from_slot = slot_index
+ drag_active = true
+ # Icon am Cursor erstellen aus dem aktuellen Slot-Icon
+ var current_texture = slot_icons[slot_index].texture
+ if current_texture == null:
+ drag_active = false
+ drag_from_slot = -1
+ return
+ drag_icon = TextureRect.new()
+ drag_icon.texture = current_texture
+ drag_icon.custom_minimum_size = Vector2(40, 40)
+ drag_icon.size = Vector2(40, 40)
+ drag_icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ drag_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ drag_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ var drag_layer = CanvasLayer.new()
+ drag_layer.name = "DragLayer"
+ drag_layer.layer = 200
+ drag_layer.add_child(drag_icon)
+ get_tree().root.add_child(drag_layer)
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+
+func _process(_delta):
+ if drag_from_slot >= 0 and drag_icon:
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+ update_drag_hover(get_viewport().get_mouse_position())
+
+func _input(event):
+ if not drag_active or drag_from_slot < 0:
+ return
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
+ _end_actionbar_drag()
+
+func _end_actionbar_drag():
+ var drop_slot = get_slot_at_position(get_viewport().get_mouse_position())
+ if drop_slot >= 0 and drop_slot <= 8 and drop_slot != drag_from_slot:
+ # Auf anderen Slot verschoben -> swap
+ slot_drag_swapped.emit(drag_from_slot, drop_slot)
+ elif drop_slot < 0 or drop_slot > 8:
+ # Außerhalb gedroppt -> aus Leiste entfernen
+ slot_drag_removed.emit(drag_from_slot)
+ # Aufräumen
+ _clear_drag_highlight()
+ if drag_icon:
+ var drag_layer = drag_icon.get_parent()
+ drag_layer.queue_free()
+ drag_icon = null
+ drag_active = false
+ drag_from_slot = -1
+
+# Slot-Klick Handler
+func _on_slot_clicked(slot_index: int):
+ if drag_active:
+ return # Während Drag keine Klicks
+ set_active_slot(slot_index)
+ slot_clicked.emit(slot_index)
+
+# Icon für einen Slot setzen
+func set_slot_icon(slot_index: int, icon_path: String):
+ if slot_index >= 0 and slot_index < 9:
+ var texture = load(icon_path)
+ if texture:
+ slot_icons[slot_index].texture = texture
+ else:
+ print("Icon nicht gefunden: ", icon_path)
+
+# Cooldown für einen Slot anzeigen (remaining_time in Sekunden)
+func set_slot_cooldown(slot_index: int, remaining_time: float):
+ if slot_index < 0 or slot_index >= 9:
+ return
+
+ if remaining_time > 0:
+ slot_cooldown_overlays[slot_index].visible = true
+ slot_cooldown_labels[slot_index].visible = true
+ slot_cooldown_labels[slot_index].text = "%.1f" % remaining_time
+ else:
+ slot_cooldown_overlays[slot_index].visible = false
+ slot_cooldown_labels[slot_index].visible = false
+
+# Level/XP UI erstellen
+func _create_level_ui():
+ var control = $Control
+
+ # Level Label
+ level_label = Label.new()
+ level_label.name = "LevelLabel"
+ level_label.position = Vector2(20, 55)
+ level_label.add_theme_font_size_override("font_size", 14)
+ level_label.text = "Level 1"
+ control.add_child(level_label)
+
+ # XP Bar
+ xp_bar = ProgressBar.new()
+ xp_bar.name = "XPBar"
+ xp_bar.position = Vector2(80, 55)
+ xp_bar.size = Vector2(140, 18)
+ xp_bar.show_percentage = false
+ xp_bar.value = 0
+
+ # XP Bar Farbe (blau)
+ var xp_style = StyleBoxFlat.new()
+ xp_style.bg_color = Color(0.2, 0.4, 0.9, 1.0)
+ xp_bar.add_theme_stylebox_override("fill", xp_style)
+
+ control.add_child(xp_bar)
+
+ # Ressourcen-Bar (Mana/Energie/Wut) - unter HP-Bar
+ resource_bar = ProgressBar.new()
+ resource_bar.name = "ResourceBar"
+ resource_bar.position = Vector2(20, 50)
+ resource_bar.size = Vector2(200, 20)
+ resource_bar.show_percentage = false
+ resource_bar.value = 0
+ resource_bar.visible = false # Nur sichtbar wenn Klasse Ressource hat
+
+ var resource_style = StyleBoxFlat.new()
+ resource_style.bg_color = Color(0.2, 0.3, 0.9, 1.0) # Blau für Mana (Standard)
+ resource_bar.add_theme_stylebox_override("fill", resource_style)
+
+ resource_label = Label.new()
+ resource_label.name = "ResourceLabel"
+ resource_label.size = Vector2(200, 20)
+ resource_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ resource_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+
+ resource_bar.add_child(resource_label)
+ control.add_child(resource_bar)
+
+ # Level/XP etwas runter verschieben wegen Ressourcen-Bar
+ level_label.position = Vector2(20, 75)
+ xp_bar.position = Vector2(80, 75)
+
+ # Gold Label
+ gold_label = Label.new()
+ gold_label.name = "GoldLabel"
+ gold_label.position = Vector2(20, 98)
+ gold_label.add_theme_font_size_override("font_size", 14)
+ gold_label.add_theme_color_override("font_color", Color(1, 0.85, 0, 1))
+ gold_label.text = "0 Gold"
+ control.add_child(gold_label)
+
+ # Castbar — direkt über der Aktionsleiste positioniert
+ _create_castbar()
+
+# Gold aktualisieren
+func update_gold(amount: int):
+ if gold_label:
+ gold_label.text = str(amount) + " Gold"
+
+# Ressourcen-Leiste aktualisieren (Mana/Energie/Wut)
+func update_resource(current: int, maximum: int, resource_name: String):
+ if resource_bar == null:
+ return
+ if maximum <= 0:
+ resource_bar.visible = false
+ return
+ resource_bar.visible = true
+ resource_bar.max_value = maximum
+ resource_bar.value = current
+ resource_label.text = str(current) + " / " + str(maximum)
+
+ # Farbe je nach Ressourcen-Typ
+ var style = resource_bar.get_theme_stylebox("fill") as StyleBoxFlat
+ if style:
+ match resource_name:
+ "Mana":
+ style.bg_color = Color(0.2, 0.3, 0.9, 1.0) # Blau
+ "Energie":
+ style.bg_color = Color(0.9, 0.8, 0.1, 1.0) # Gelb
+ "Wut":
+ style.bg_color = Color(0.8, 0.15, 0.1, 1.0) # Rot
+
+# Icon-Textur direkt setzen (für Consumables)
+func set_slot_icon_texture(slot_index: int, texture: Texture2D):
+ if slot_index >= 0 and slot_index < 9:
+ slot_icons[slot_index].texture = texture
+
+# Slot-Icon entfernen
+func clear_slot_icon(slot_index: int):
+ if slot_index >= 0 and slot_index < 9:
+ slot_icons[slot_index].texture = null
+
+# Stack-Anzahl auf Slot anzeigen
+func set_slot_stack_count(slot_index: int, count: int):
+ if slot_index < 0 or slot_index >= 9:
+ return
+ if count > 1:
+ slot_stack_labels[slot_index].text = str(count)
+ slot_stack_labels[slot_index].visible = true
+ else:
+ slot_stack_labels[slot_index].visible = false
# HP-Leiste und Text aktualisieren
func update_health(current_hp, max_hp):
@@ -24,6 +315,14 @@ func update_health(current_hp, max_hp):
health_bar.value = current_hp
health_label.text = str(current_hp) + " / " + str(max_hp)
+# Level und XP aktualisieren
+func update_level(level: int, current_xp: int, xp_to_next: int):
+ if level_label:
+ level_label.text = "Lv " + str(level)
+ if xp_bar:
+ xp_bar.max_value = xp_to_next
+ xp_bar.value = current_xp
+
# Aktions-Slot kurz golden hervorheben (0.1s)
func set_active_slot(index):
action_slots[active_slot].self_modulate = Color(1, 1, 1)
@@ -31,3 +330,120 @@ func set_active_slot(index):
action_slots[active_slot].self_modulate = Color(1, 0.8, 0)
await get_tree().create_timer(0.1).timeout
action_slots[active_slot].self_modulate = Color(1, 1, 1)
+
+# Drag & Drop: Highlight aktivieren/deaktivieren
+func set_drag_active(active: bool):
+ drag_active = active
+ if not active:
+ # Alle Highlights entfernen
+ _clear_drag_highlight()
+
+# Drag & Drop: Hover über Slots prüfen und gelben Rand setzen
+func update_drag_hover(mouse_pos: Vector2):
+ if not drag_active:
+ return
+ var hovered = get_slot_at_position(mouse_pos)
+ if hovered == drag_highlight_slot:
+ return # Keine Änderung
+ # Alten Highlight entfernen
+ _clear_drag_highlight()
+ # Neuen Highlight setzen (nur Slots 2-8)
+ if hovered >= 0 and hovered <= 8:
+ drag_highlight_slot = hovered
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.15, 0.15, 0.15)
+ style.border_color = Color(1, 0.9, 0, 1) # Gelber Rand
+ style.set_border_width_all(3)
+ action_slots[hovered].add_theme_stylebox_override("panel", style)
+
+func _clear_drag_highlight():
+ if drag_highlight_slot >= 0 and drag_highlight_slot < 9:
+ action_slots[drag_highlight_slot].remove_theme_stylebox_override("panel")
+ drag_highlight_slot = -1
+
+# Gibt den Slot-Index zurück wenn mouse_pos über einem Action-Slot liegt, sonst -1
+func get_slot_at_position(mouse_pos: Vector2) -> int:
+ for i in range(9):
+ var slot = action_slots[i]
+ var rect = slot.get_global_rect()
+ if rect.has_point(mouse_pos):
+ return i
+ return -1
+
+func _create_castbar():
+ # Eigene CanvasLayer für die Castbar
+ var castbar_layer = CanvasLayer.new()
+ castbar_layer.name = "CastbarLayer"
+ castbar_layer.layer = 10
+ add_child(castbar_layer)
+
+ castbar_container = PanelContainer.new()
+ castbar_container.name = "CastbarContainer"
+ castbar_container.visible = false
+ castbar_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
+
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.1, 0.1, 0.1, 0.85)
+ style.set_border_width_all(2)
+ style.border_color = Color(0.6, 0.6, 0.6)
+ style.set_corner_radius_all(4)
+ castbar_container.add_theme_stylebox_override("panel", style)
+
+ var vbox = VBoxContainer.new()
+ vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ castbar_container.add_child(vbox)
+
+ castbar_label = Label.new()
+ castbar_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ castbar_label.add_theme_font_size_override("font_size", 13)
+ castbar_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ vbox.add_child(castbar_label)
+
+ castbar = ProgressBar.new()
+ castbar.custom_minimum_size = Vector2(300, 16)
+ castbar.show_percentage = false
+ castbar.mouse_filter = Control.MOUSE_FILTER_IGNORE
+
+ var fill = StyleBoxFlat.new()
+ fill.bg_color = Color(1.0, 0.7, 0.0, 1.0)
+ fill.set_corner_radius_all(2)
+ castbar.add_theme_stylebox_override("fill", fill)
+
+ var bg = StyleBoxFlat.new()
+ bg.bg_color = Color(0.2, 0.2, 0.2, 1.0)
+ bg.set_corner_radius_all(2)
+ castbar.add_theme_stylebox_override("background", bg)
+
+ vbox.add_child(castbar)
+ castbar_layer.add_child(castbar_container)
+
+# Castbar anzeigen
+func show_castbar(spell_name: String, cast_time: float):
+ if castbar_container == null:
+ return
+ # Spell-Name übersetzen
+ var display_name = spell_name
+ match spell_name:
+ "frostbolt": display_name = "Frostblitz"
+ castbar_label.text = display_name + " (" + "%.1f" % cast_time + "s)"
+ castbar.max_value = cast_time
+ castbar.value = 0
+ # Mittig über der Aktionsleiste positionieren
+ var viewport_size = get_viewport().get_visible_rect().size
+ castbar_container.position = Vector2(viewport_size.x / 2 - 160, viewport_size.y - 120)
+ castbar_container.visible = true
+
+# Castbar Fortschritt aktualisieren
+func update_castbar(elapsed: float, total: float):
+ if castbar == null:
+ return
+ castbar.value = elapsed
+ var remaining = total - elapsed
+ if remaining < 0:
+ remaining = 0
+ castbar_label.text = castbar_label.text.split("(")[0].strip_edges() + " (%.1f" % remaining + "s)"
+
+# Castbar verstecken
+func hide_castbar():
+ if castbar_container:
+ castbar_container.visible = false
diff --git a/icons/autoattack_icon.svg b/icons/autoattack_icon.svg
new file mode 100644
index 0000000..dc7b97d
--- /dev/null
+++ b/icons/autoattack_icon.svg
@@ -0,0 +1,25 @@
+
diff --git a/icons/autoattack_icon.svg.import b/icons/autoattack_icon.svg.import
new file mode 100644
index 0000000..0549514
--- /dev/null
+++ b/icons/autoattack_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bkujmu8r6370p"
+path="res://.godot/imported/autoattack_icon.svg-8c48165bd13a879d5f6cdbc811e19882.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/autoattack_icon.svg"
+dest_files=["res://.godot/imported/autoattack_icon.svg-8c48165bd13a879d5f6cdbc811e19882.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/frostbolt_icon.svg b/icons/frostbolt_icon.svg
new file mode 100644
index 0000000..ecde089
--- /dev/null
+++ b/icons/frostbolt_icon.svg
@@ -0,0 +1,14 @@
+
diff --git a/icons/frostbolt_icon.svg.import b/icons/frostbolt_icon.svg.import
new file mode 100644
index 0000000..c88df9f
--- /dev/null
+++ b/icons/frostbolt_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d3lnef1xxgol3"
+path="res://.godot/imported/frostbolt_icon.svg-97e9c6299dcf36b8065da5f6427efafe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/frostbolt_icon.svg"
+dest_files=["res://.godot/imported/frostbolt_icon.svg-97e9c6299dcf36b8065da5f6427efafe.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/heavy_strike_icon.svg b/icons/heavy_strike_icon.svg
new file mode 100644
index 0000000..ce005ff
--- /dev/null
+++ b/icons/heavy_strike_icon.svg
@@ -0,0 +1,26 @@
+
diff --git a/icons/heavy_strike_icon.svg.import b/icons/heavy_strike_icon.svg.import
new file mode 100644
index 0000000..790abfa
--- /dev/null
+++ b/icons/heavy_strike_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dqkaowq4cvtbl"
+path="res://.godot/imported/heavy_strike_icon.svg-ae86ca74d5a61e67598cea5812ee34ac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/heavy_strike_icon.svg"
+dest_files=["res://.godot/imported/heavy_strike_icon.svg-ae86ca74d5a61e67598cea5812ee34ac.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/hp_potion_icon.svg b/icons/hp_potion_icon.svg
new file mode 100644
index 0000000..0103703
--- /dev/null
+++ b/icons/hp_potion_icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/icons/hp_potion_icon.svg.import b/icons/hp_potion_icon.svg.import
new file mode 100644
index 0000000..921a909
--- /dev/null
+++ b/icons/hp_potion_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cih3xmw6q88um"
+path="res://.godot/imported/hp_potion_icon.svg-fba9724ef0f842c8b57011d6e4f03fb3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/hp_potion_icon.svg"
+dest_files=["res://.godot/imported/hp_potion_icon.svg-fba9724ef0f842c8b57011d6e4f03fb3.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/iron_helm_icon.svg b/icons/iron_helm_icon.svg
new file mode 100644
index 0000000..9ba19b8
--- /dev/null
+++ b/icons/iron_helm_icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/icons/iron_helm_icon.svg.import b/icons/iron_helm_icon.svg.import
new file mode 100644
index 0000000..7a9090f
--- /dev/null
+++ b/icons/iron_helm_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c8x8d055wjcpu"
+path="res://.godot/imported/iron_helm_icon.svg-f08644c5491a1e9fd669ad08b500cdc1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/iron_helm_icon.svg"
+dest_files=["res://.godot/imported/iron_helm_icon.svg-f08644c5491a1e9fd669ad08b500cdc1.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/iron_sword_icon.svg b/icons/iron_sword_icon.svg
new file mode 100644
index 0000000..d590fae
--- /dev/null
+++ b/icons/iron_sword_icon.svg
@@ -0,0 +1,11 @@
+
diff --git a/icons/iron_sword_icon.svg.import b/icons/iron_sword_icon.svg.import
new file mode 100644
index 0000000..ab44f1d
--- /dev/null
+++ b/icons/iron_sword_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bi7sh66txlj1c"
+path="res://.godot/imported/iron_sword_icon.svg-7666556b18881bfee488b89a46108232.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/iron_sword_icon.svg"
+dest_files=["res://.godot/imported/iron_sword_icon.svg-7666556b18881bfee488b89a46108232.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/leather_chest_icon.svg b/icons/leather_chest_icon.svg
new file mode 100644
index 0000000..b317dba
--- /dev/null
+++ b/icons/leather_chest_icon.svg
@@ -0,0 +1,14 @@
+
diff --git a/icons/leather_chest_icon.svg.import b/icons/leather_chest_icon.svg.import
new file mode 100644
index 0000000..f4e624d
--- /dev/null
+++ b/icons/leather_chest_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c6kmscwfv0ssa"
+path="res://.godot/imported/leather_chest_icon.svg-c139cc7810b1521bcabf2843303318b5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/leather_chest_icon.svg"
+dest_files=["res://.godot/imported/leather_chest_icon.svg-c139cc7810b1521bcabf2843303318b5.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/mana_potion_icon.svg b/icons/mana_potion_icon.svg
new file mode 100644
index 0000000..8b3e036
--- /dev/null
+++ b/icons/mana_potion_icon.svg
@@ -0,0 +1,12 @@
+
diff --git a/icons/mana_potion_icon.svg.import b/icons/mana_potion_icon.svg.import
new file mode 100644
index 0000000..1d9cb42
--- /dev/null
+++ b/icons/mana_potion_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dc6ai8ec7g20"
+path="res://.godot/imported/mana_potion_icon.svg-a748c0946003b46a082e0546c17e9f84.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/mana_potion_icon.svg"
+dest_files=["res://.godot/imported/mana_potion_icon.svg-a748c0946003b46a082e0546c17e9f84.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/steel_sword_icon.svg b/icons/steel_sword_icon.svg
new file mode 100644
index 0000000..56749aa
--- /dev/null
+++ b/icons/steel_sword_icon.svg
@@ -0,0 +1,11 @@
+
diff --git a/icons/steel_sword_icon.svg.import b/icons/steel_sword_icon.svg.import
new file mode 100644
index 0000000..68eeaa2
--- /dev/null
+++ b/icons/steel_sword_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bsj8lgen1giiw"
+path="res://.godot/imported/steel_sword_icon.svg-e1e6ede998445ecfd316dac81b83a7c5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/steel_sword_icon.svg"
+dest_files=["res://.godot/imported/steel_sword_icon.svg-e1e6ede998445ecfd316dac81b83a7c5.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/wand_icon.svg b/icons/wand_icon.svg
new file mode 100644
index 0000000..223966d
--- /dev/null
+++ b/icons/wand_icon.svg
@@ -0,0 +1,16 @@
+
diff --git a/icons/wand_icon.svg.import b/icons/wand_icon.svg.import
new file mode 100644
index 0000000..12c6c1e
--- /dev/null
+++ b/icons/wand_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dsa2obffh60qx"
+path="res://.godot/imported/wand_icon.svg-5bbc5f62e64992d218efbb5a4fb81c8f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/wand_icon.svg"
+dest_files=["res://.godot/imported/wand_icon.svg-5bbc5f62e64992d218efbb5a4fb81c8f.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/wooden_shield_icon.svg b/icons/wooden_shield_icon.svg
new file mode 100644
index 0000000..ca48b76
--- /dev/null
+++ b/icons/wooden_shield_icon.svg
@@ -0,0 +1,17 @@
+
diff --git a/icons/wooden_shield_icon.svg.import b/icons/wooden_shield_icon.svg.import
new file mode 100644
index 0000000..f37f763
--- /dev/null
+++ b/icons/wooden_shield_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dovodjtj2gb2e"
+path="res://.godot/imported/wooden_shield_icon.svg-9243b141ac1930d785b6e64e9bdd401e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/wooden_shield_icon.svg"
+dest_files=["res://.godot/imported/wooden_shield_icon.svg-9243b141ac1930d785b6e64e9bdd401e.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/wooden_staff_icon.svg b/icons/wooden_staff_icon.svg
new file mode 100644
index 0000000..e1d59f4
--- /dev/null
+++ b/icons/wooden_staff_icon.svg
@@ -0,0 +1,14 @@
+
diff --git a/icons/wooden_staff_icon.svg.import b/icons/wooden_staff_icon.svg.import
new file mode 100644
index 0000000..fb43f97
--- /dev/null
+++ b/icons/wooden_staff_icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dts32a6fpdc84"
+path="res://.godot/imported/wooden_staff_icon.svg-9178c7c24ac6ded32a37ea646c50397f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icons/wooden_staff_icon.svg"
+dest_files=["res://.godot/imported/wooden_staff_icon.svg-9178c7c24ac6ded32a37ea646c50397f.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=false
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/inventory.gd b/inventory.gd
new file mode 100644
index 0000000..f39a816
--- /dev/null
+++ b/inventory.gd
@@ -0,0 +1,128 @@
+# Inventory.gd
+# Verwaltet das Spieler-Inventar mit Items (Equipment + Consumables) und Gold
+extends Resource
+class_name Inventory
+
+signal inventory_changed
+signal gold_changed(new_amount: int)
+
+const MAX_SLOTS = 20 # Maximale Inventarplätze
+
+var items: Array = [] # Equipment oder Consumable
+var gold: int = 0
+
+func _init():
+ items = []
+ gold = 0
+
+# Item hinzufügen - gibt true zurück wenn erfolgreich
+# Consumables werden gestackt wenn möglich
+func add_item(item) -> bool:
+ # Consumable stacking
+ if item is Consumable:
+ for existing in items:
+ if existing is Consumable and existing.item_name == item.item_name:
+ var space = existing.max_stack - existing.stack_size
+ if space > 0:
+ var transfer = mini(item.stack_size, space)
+ existing.stack_size += transfer
+ item.stack_size -= transfer
+ if item.stack_size <= 0:
+ inventory_changed.emit()
+ print("Item gestackt: ", existing.item_name, " x", existing.stack_size)
+ return true
+
+ if items.size() >= MAX_SLOTS:
+ print("Inventar voll!")
+ return false
+ items.append(item)
+ inventory_changed.emit()
+ if item is Consumable:
+ print("Item erhalten: ", item.item_name, " x", item.stack_size)
+ else:
+ print("Item erhalten: ", item.item_name)
+ return true
+
+# Item an Index entfernen
+func remove_item_at(index: int):
+ if index < 0 or index >= items.size():
+ return null
+ var item = items[index]
+ items.remove_at(index)
+ inventory_changed.emit()
+ return item
+
+# Item direkt entfernen
+func remove_item(item) -> bool:
+ var index = items.find(item)
+ if index == -1:
+ return false
+ items.remove_at(index)
+ inventory_changed.emit()
+ return true
+
+# Gold hinzufügen
+func add_gold(amount: int):
+ gold += amount
+ gold_changed.emit(gold)
+ print("+", amount, " Gold (Gesamt: ", gold, ")")
+
+# Gold ausgeben - gibt true zurück wenn genug vorhanden
+func spend_gold(amount: int) -> bool:
+ if gold < amount:
+ print("Nicht genug Gold!")
+ return false
+ gold -= amount
+ gold_changed.emit(gold)
+ return true
+
+# Prüfen ob Inventar voll ist
+func is_full() -> bool:
+ return items.size() >= MAX_SLOTS
+
+# Anzahl freier Slots
+func free_slots() -> int:
+ return MAX_SLOTS - items.size()
+
+# Item an Index holen
+func get_item(index: int):
+ if index < 0 or index >= items.size():
+ return null
+ return items[index]
+
+# Zwei Items im Inventar tauschen
+func swap_items(index_a: int, index_b: int):
+ if index_a < 0 or index_b < 0:
+ return
+ if index_a >= items.size() or index_b >= items.size():
+ return
+ var temp = items[index_a]
+ items[index_a] = items[index_b]
+ items[index_b] = temp
+ inventory_changed.emit()
+
+# Item an bestimmten Index verschieben (von aktuellem Platz)
+func move_item(from_index: int, to_index: int):
+ if from_index < 0 or from_index >= items.size():
+ return
+ if to_index < 0 or to_index >= MAX_SLOTS:
+ return
+ if from_index == to_index:
+ return
+ if to_index < items.size():
+ # Ziel belegt -> tauschen
+ swap_items(from_index, to_index)
+ else:
+ # Ziel leer -> Item verschieben
+ var item = items[from_index]
+ items.remove_at(from_index)
+ # Index anpassen falls nötig
+ if to_index >= items.size():
+ items.append(item)
+ else:
+ items.insert(to_index, item)
+ inventory_changed.emit()
+
+# Anzahl Items
+func item_count() -> int:
+ return items.size()
diff --git a/inventory.gd.uid b/inventory.gd.uid
new file mode 100644
index 0000000..9e173d9
--- /dev/null
+++ b/inventory.gd.uid
@@ -0,0 +1 @@
+uid://b7x1jr36w0bfg
diff --git a/inventory_panel.gd b/inventory_panel.gd
new file mode 100644
index 0000000..e274575
--- /dev/null
+++ b/inventory_panel.gd
@@ -0,0 +1,327 @@
+# InventoryPanel.gd
+# UI für das Spieler-Inventar (Equipment + Consumables)
+extends CanvasLayer
+
+signal item_selected(item, index: int)
+
+var panel_visible = false
+var player = null
+
+# Drag & Drop
+var dragging = false
+var drag_item = null # Equipment oder Consumable
+var drag_from_index: int = -1 # Inventar-Index von dem gedraggt wird
+var drag_icon: TextureRect = null
+var drag_highlight_slot: int = -1 # Aktuell hervorgehobener Inventar-Slot
+
+@onready var panel = $Panel
+@onready var gold_label = $Panel/VBoxContainer/Header/GoldLabel
+@onready var item_grid = $Panel/VBoxContainer/ScrollContainer/ItemGrid
+
+const SLOT_SIZE = 50
+
+func _ready():
+ panel.visible = false
+
+func setup(p):
+ player = p
+ if player and player.inventory:
+ player.inventory.inventory_changed.connect(_on_inventory_changed)
+ player.inventory.gold_changed.connect(_on_gold_changed)
+ _refresh_inventory()
+
+func toggle():
+ panel_visible = !panel_visible
+ panel.visible = panel_visible
+ if panel_visible:
+ _refresh_inventory()
+
+func _on_inventory_changed():
+ if panel_visible:
+ _refresh_inventory()
+
+func _on_gold_changed(new_amount: int):
+ gold_label.text = str(new_amount) + " Gold"
+
+func _refresh_inventory():
+ if player == null or player.inventory == null:
+ return
+
+ # Gold aktualisieren
+ gold_label.text = str(player.inventory.gold) + " Gold"
+
+ # Alte Slots entfernen
+ for child in item_grid.get_children():
+ child.queue_free()
+
+ # Slots erstellen (immer MAX_SLOTS anzeigen)
+ for i in range(Inventory.MAX_SLOTS):
+ var slot = _create_slot(i)
+ item_grid.add_child(slot)
+
+func _create_slot(index: int) -> Panel:
+ var slot = Panel.new()
+ slot.custom_minimum_size = Vector2(SLOT_SIZE, SLOT_SIZE)
+
+ # Slot-Hintergrund stylen
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.15, 0.15, 0.15)
+ style.border_color = Color(0.3, 0.3, 0.3)
+ style.set_border_width_all(1)
+ slot.add_theme_stylebox_override("panel", style)
+
+ # Alle Slots brauchen gui_input für Drop-Erkennung
+ slot.gui_input.connect(_on_slot_input.bind(index))
+
+ # Item vorhanden?
+ if player.inventory and index < player.inventory.item_count():
+ var item = player.inventory.get_item(index)
+ if item:
+ var item_icon = null
+ if item is Consumable:
+ item_icon = item.icon
+ elif item is Equipment:
+ item_icon = item.icon
+
+ if item_icon:
+ var icon = TextureRect.new()
+ icon.texture = item_icon
+ icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.custom_minimum_size = Vector2(SLOT_SIZE - 4, SLOT_SIZE - 4)
+ icon.position = Vector2(2, 2)
+ icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ slot.add_child(icon)
+ else:
+ # Fallback: Text
+ var label = Label.new()
+ label.text = item.item_name.substr(0, 3)
+ label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ label.add_theme_font_size_override("font_size", 10)
+ if item is Equipment:
+ label.modulate = Equipment.get_rarity_color(item.rarity)
+ label.anchors_preset = Control.PRESET_FULL_RECT
+ label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ slot.add_child(label)
+
+ # Stack-Count für Consumables
+ if item is Consumable and item.stack_size > 1:
+ var stack_label = Label.new()
+ stack_label.text = str(item.stack_size)
+ stack_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ stack_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
+ stack_label.add_theme_font_size_override("font_size", 11)
+ stack_label.size = Vector2(SLOT_SIZE - 4, SLOT_SIZE - 4)
+ stack_label.position = Vector2(0, 0)
+ stack_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ slot.add_child(stack_label)
+
+ # Rahmen
+ if item is Equipment:
+ style.border_color = Equipment.get_rarity_color(item.rarity)
+ elif item is Consumable:
+ style.border_color = Color(0.3, 0.7, 0.3)
+ style.set_border_width_all(2)
+
+ # Tooltip
+ slot.tooltip_text = _get_item_tooltip(item)
+
+ return slot
+
+func _on_slot_input(event: InputEvent, index: int):
+ if event is InputEventMouseButton and event.pressed:
+ var item = null
+ if player.inventory and index < player.inventory.item_count():
+ item = player.inventory.get_item(index)
+
+ if item == null:
+ return
+
+ if item is Equipment:
+ if event.button_index == MOUSE_BUTTON_RIGHT:
+ # Rechtsklick auf Equipment: Anlegen
+ if player:
+ var old_item = player.equip_item(item)
+ player.inventory.remove_item(item)
+ if old_item:
+ player.inventory.add_item(old_item)
+ _refresh_inventory()
+ elif event.button_index == MOUSE_BUTTON_LEFT:
+ # Linksklick: Drag starten
+ _start_drag(item, index)
+ elif item is Consumable:
+ if event.button_index == MOUSE_BUTTON_RIGHT:
+ # Rechtsklick auf Consumable: Direkt benutzen
+ if player:
+ if player.use_consumable(item):
+ if item.stack_size <= 0:
+ player.inventory.remove_item(item)
+ _refresh_inventory()
+ player._update_action_bar_stacks()
+ elif event.button_index == MOUSE_BUTTON_LEFT:
+ # Linksklick: Drag starten
+ _start_drag(item, index)
+
+# Drag & Drop System
+func _start_drag(item, index: int):
+ dragging = true
+ drag_item = item
+ drag_from_index = index
+ # Icon am Mauszeiger erstellen
+ var tex = item.icon if item.icon else null
+ drag_icon = TextureRect.new()
+ if tex:
+ drag_icon.texture = tex
+ drag_icon.custom_minimum_size = Vector2(40, 40)
+ drag_icon.size = Vector2(40, 40)
+ drag_icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ drag_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ drag_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ # Eigene CanvasLayer damit Icon über allem anderen liegt
+ var drag_layer = CanvasLayer.new()
+ drag_layer.name = "DragLayer"
+ drag_layer.layer = 200
+ drag_layer.add_child(drag_icon)
+ get_tree().root.add_child(drag_layer)
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+ # Highlight auf HUD aktivieren
+ if player and player.hud:
+ player.hud.set_drag_active(true)
+
+func _process(_delta):
+ if dragging and drag_icon:
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+ var mouse_pos = get_viewport().get_mouse_position()
+ # HUD Slots highlighten
+ if player and player.hud:
+ player.hud.update_drag_hover(mouse_pos)
+ # Inventar-Slots highlighten
+ _update_inventory_hover(mouse_pos)
+
+func _input(event):
+ if not dragging:
+ return
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
+ # Maus losgelassen - Drop prüfen
+ _end_drag()
+
+func _end_drag():
+ if not dragging:
+ return
+ var mouse_pos = get_viewport().get_mouse_position()
+
+ # Prüfen ob über einem Inventar-Slot
+ var inv_slot = _get_inventory_slot_at(mouse_pos)
+ if inv_slot >= 0 and inv_slot != drag_from_index:
+ # Innerhalb Inventar verschieben/tauschen
+ player.inventory.move_item(drag_from_index, inv_slot)
+ else:
+ # Prüfen ob über einem Action-Slot (nur Consumables)
+ if player and player.hud:
+ var action_slot = player.hud.get_slot_at_position(mouse_pos)
+ if action_slot >= 0 and action_slot <= 8 and drag_item:
+ if drag_item is Consumable:
+ player.assign_to_action_bar(action_slot, drag_item)
+ print(drag_item.item_name + " auf Aktionsleiste Slot " + str(action_slot + 1) + " gelegt")
+
+ # HUD Highlight deaktivieren
+ if player and player.hud:
+ player.hud.set_drag_active(false)
+ # Inventar Highlight entfernen
+ _clear_inventory_highlight()
+ # Aufräumen
+ if drag_icon:
+ var drag_layer = drag_icon.get_parent()
+ drag_layer.queue_free()
+ drag_icon = null
+ dragging = false
+ drag_item = null
+ drag_from_index = -1
+
+# Inventar-Slot unter Mausposition finden
+func _get_inventory_slot_at(mouse_pos: Vector2) -> int:
+ for i in range(item_grid.get_child_count()):
+ var slot = item_grid.get_child(i)
+ var rect = slot.get_global_rect()
+ if rect.has_point(mouse_pos):
+ return i
+ return -1
+
+# Inventar-Slot Highlight während Drag
+func _update_inventory_hover(mouse_pos: Vector2):
+ var hovered = _get_inventory_slot_at(mouse_pos)
+ if hovered == drag_highlight_slot:
+ return
+ _clear_inventory_highlight()
+ if hovered >= 0 and hovered != drag_from_index:
+ drag_highlight_slot = hovered
+ var slot = item_grid.get_child(hovered)
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.15, 0.15, 0.15)
+ style.border_color = Color(1, 0.9, 0, 1) # Gelber Rand
+ style.set_border_width_all(3)
+ slot.add_theme_stylebox_override("panel", style)
+
+func _clear_inventory_highlight():
+ if drag_highlight_slot >= 0 and drag_highlight_slot < item_grid.get_child_count():
+ # Style zurücksetzen
+ var slot = item_grid.get_child(drag_highlight_slot)
+ var item = null
+ if player.inventory and drag_highlight_slot < player.inventory.item_count():
+ item = player.inventory.get_item(drag_highlight_slot)
+ var style = StyleBoxFlat.new()
+ style.bg_color = Color(0.15, 0.15, 0.15)
+ if item is Equipment:
+ style.border_color = Equipment.get_rarity_color(item.rarity)
+ style.set_border_width_all(2)
+ elif item is Consumable:
+ style.border_color = Color(0.3, 0.7, 0.3)
+ style.set_border_width_all(2)
+ else:
+ style.border_color = Color(0.3, 0.3, 0.3)
+ style.set_border_width_all(1)
+ slot.add_theme_stylebox_override("panel", style)
+ drag_highlight_slot = -1
+
+func _get_item_tooltip(item) -> String:
+ if item is Consumable:
+ return _get_consumable_tooltip(item)
+ elif item is Equipment:
+ return _get_equipment_tooltip(item)
+ return item.item_name
+
+func _get_consumable_tooltip(item: Consumable) -> String:
+ var tooltip = item.item_name + "\n"
+ tooltip += "Verbrauchbar\n"
+ tooltip += "---\n"
+ tooltip += item.get_effect_text() + "\n"
+ tooltip += "Cooldown: " + str(item.cooldown) + "s\n"
+ tooltip += "Anzahl: " + str(item.stack_size) + "/" + str(item.max_stack) + "\n"
+ tooltip += "\n[Rechtsklick: Benutzen]\n[Linksklick: Ziehen]"
+ return tooltip
+
+func _get_equipment_tooltip(item: Equipment) -> String:
+ var tooltip = item.item_name + "\n"
+ tooltip += Equipment.get_slot_name(item.slot) + "\n"
+ tooltip += "---\n"
+
+ if item.slot == Equipment.Slot.WEAPON:
+ tooltip += "Schaden: " + str(item.min_damage) + "-" + str(item.max_damage) + "\n"
+ tooltip += "Tempo: " + str(item.attack_speed) + "s\n"
+
+ if item.armor > 0:
+ tooltip += "Rüstung: +" + str(item.armor) + "\n"
+ if item.strength > 0:
+ tooltip += "Stärke: +" + str(item.strength) + "\n"
+ if item.agility > 0:
+ tooltip += "Beweglichkeit: +" + str(item.agility) + "\n"
+ if item.intelligence > 0:
+ tooltip += "Intelligenz: +" + str(item.intelligence) + "\n"
+ if item.stamina > 0:
+ tooltip += "Ausdauer: +" + str(item.stamina) + "\n"
+ if item.haste > 0:
+ tooltip += "Tempo: +" + str(int(item.haste * 100)) + "%\n"
+
+ tooltip += "\n[Rechtsklick zum Anlegen]"
+ return tooltip
diff --git a/inventory_panel.gd.uid b/inventory_panel.gd.uid
new file mode 100644
index 0000000..2be1619
--- /dev/null
+++ b/inventory_panel.gd.uid
@@ -0,0 +1 @@
+uid://bjmcu0tyxqxmd
diff --git a/inventory_panel.tscn b/inventory_panel.tscn
new file mode 100644
index 0000000..b31478d
--- /dev/null
+++ b/inventory_panel.tscn
@@ -0,0 +1,63 @@
+[gd_scene format=3 uid="uid://inventory_panel"]
+
+[ext_resource type="Script" path="res://inventory_panel.gd" id="1_panel"]
+
+[node name="InventoryPanel" type="CanvasLayer"]
+script = ExtResource("1_panel")
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 6
+anchor_left = 1.0
+anchor_top = 0.5
+anchor_right = 1.0
+anchor_bottom = 0.5
+offset_left = -320.0
+offset_top = -250.0
+offset_right = -20.0
+offset_bottom = 250.0
+grow_horizontal = 0
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 10.0
+offset_top = 10.0
+offset_right = -10.0
+offset_bottom = -10.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 8
+
+[node name="Header" type="HBoxContainer" parent="Panel/VBoxContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="Panel/VBoxContainer/Header"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 20
+text = "Inventar"
+
+[node name="GoldLabel" type="Label" parent="Panel/VBoxContainer/Header"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+theme_override_colors/font_color = Color(1, 0.85, 0, 1)
+text = "0 Gold"
+horizontal_alignment = 2
+
+[node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"]
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ItemGrid" type="GridContainer" parent="Panel/VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/h_separation = 5
+theme_override_constants/v_separation = 5
+columns = 5
diff --git a/loot_entry.gd b/loot_entry.gd
new file mode 100644
index 0000000..875addb
--- /dev/null
+++ b/loot_entry.gd
@@ -0,0 +1,7 @@
+# LootEntry.gd
+# Ein einzelner Eintrag in einer LootTable
+extends Resource
+class_name LootEntry
+
+@export var item: Resource # Equipment oder Consumable
+@export_range(0.0, 1.0) var drop_chance: float = 0.1 # 10% Standard
diff --git a/loot_entry.gd.uid b/loot_entry.gd.uid
new file mode 100644
index 0000000..1b7af3a
--- /dev/null
+++ b/loot_entry.gd.uid
@@ -0,0 +1 @@
+uid://cx2w8nkuoylv5
diff --git a/loot_table.gd b/loot_table.gd
new file mode 100644
index 0000000..44e6e7e
--- /dev/null
+++ b/loot_table.gd
@@ -0,0 +1,30 @@
+# LootTable.gd
+# Definiert mögliche Drops eines Gegners
+extends Resource
+class_name LootTable
+
+# Gold-Drop
+@export var min_gold: int = 1
+@export var max_gold: int = 5
+
+# Item-Drops mit Wahrscheinlichkeiten
+@export var possible_drops: Array[LootEntry] = []
+
+# Generiert Loot basierend auf Tabelle
+func generate_loot() -> Dictionary:
+ var result = {
+ "gold": randi_range(min_gold, max_gold),
+ "items": []
+ }
+
+ for entry in possible_drops:
+ if randf() <= entry.drop_chance:
+ # Consumables kopieren damit Stacks unabhängig bleiben
+ if entry.item is Consumable:
+ var copy = entry.item.duplicate()
+ copy.stack_size = 1
+ result["items"].append(copy)
+ else:
+ result["items"].append(entry.item)
+
+ return result
diff --git a/loot_table.gd.uid b/loot_table.gd.uid
new file mode 100644
index 0000000..c5e4b89
--- /dev/null
+++ b/loot_table.gd.uid
@@ -0,0 +1 @@
+uid://dej1tpamsi71r
diff --git a/loot_tables/goblin_loot.tres b/loot_tables/goblin_loot.tres
new file mode 100644
index 0000000..8278921
--- /dev/null
+++ b/loot_tables/goblin_loot.tres
@@ -0,0 +1,38 @@
+[gd_resource type="Resource" script_class="LootTable" format=3 uid="uid://83hg0vwdovn8"]
+
+[ext_resource type="Script" uid="uid://dej1tpamsi71r" path="res://loot_table.gd" id="1"]
+[ext_resource type="Script" uid="uid://cx2w8nkuoylv5" path="res://loot_entry.gd" id="2"]
+[ext_resource type="Resource" uid="uid://vprfv2phlcbc" path="res://equipment/iron_sword.tres" id="3"]
+[ext_resource type="Resource" uid="uid://cnijhxsjmepss" path="res://equipment/leather_chest.tres" id="4"]
+[ext_resource type="Resource" uid="uid://d54jjc24pj3t" path="res://equipment/iron_helm.tres" id="5"]
+[ext_resource type="Resource" path="res://consumables/small_hp_potion.tres" id="6"]
+[ext_resource type="Resource" path="res://consumables/small_mana_potion.tres" id="7"]
+
+[sub_resource type="Resource" id="entry_1"]
+script = ExtResource("2")
+item = ExtResource("3")
+drop_chance = 0.15
+
+[sub_resource type="Resource" id="entry_2"]
+script = ExtResource("2")
+item = ExtResource("4")
+
+[sub_resource type="Resource" id="entry_3"]
+script = ExtResource("2")
+item = ExtResource("5")
+
+[sub_resource type="Resource" id="entry_4"]
+script = ExtResource("2")
+item = ExtResource("6")
+drop_chance = 0.4
+
+[sub_resource type="Resource" id="entry_5"]
+script = ExtResource("2")
+item = ExtResource("7")
+drop_chance = 0.25
+
+[resource]
+script = ExtResource("1")
+min_gold = 2
+max_gold = 8
+possible_drops = Array[ExtResource("2")]([SubResource("entry_1"), SubResource("entry_2"), SubResource("entry_3"), SubResource("entry_4"), SubResource("entry_5")])
diff --git a/loot_tables/skeleton_loot.tres b/loot_tables/skeleton_loot.tres
new file mode 100644
index 0000000..46812a3
--- /dev/null
+++ b/loot_tables/skeleton_loot.tres
@@ -0,0 +1,28 @@
+[gd_resource type="Resource" script_class="LootTable" format=3]
+
+[ext_resource type="Script" path="res://loot_table.gd" id="1"]
+[ext_resource type="Script" path="res://loot_entry.gd" id="2"]
+[ext_resource type="Resource" path="res://equipment/steel_sword.tres" id="3"]
+[ext_resource type="Resource" path="res://equipment/wooden_shield.tres" id="4"]
+[ext_resource type="Resource" path="res://equipment/iron_helm.tres" id="5"]
+
+[sub_resource type="Resource" id="entry_1"]
+script = ExtResource("2")
+item = ExtResource("3")
+drop_chance = 0.1
+
+[sub_resource type="Resource" id="entry_2"]
+script = ExtResource("2")
+item = ExtResource("4")
+drop_chance = 0.12
+
+[sub_resource type="Resource" id="entry_3"]
+script = ExtResource("2")
+item = ExtResource("5")
+drop_chance = 0.15
+
+[resource]
+script = ExtResource("1")
+min_gold = 5
+max_gold = 15
+possible_drops = [SubResource("entry_1"), SubResource("entry_2"), SubResource("entry_3")]
diff --git a/loot_window.gd b/loot_window.gd
new file mode 100644
index 0000000..f3decf2
--- /dev/null
+++ b/loot_window.gd
@@ -0,0 +1,141 @@
+# LootWindow.gd
+# Zeigt Loot eines besiegten Gegners an und lässt Spieler Items aufheben
+extends CanvasLayer
+
+var player = null
+var current_loot: Dictionary = {} # {"gold": int, "items": [Equipment]}
+var loot_world_position: Vector3 = Vector3.ZERO
+var panel_visible = false
+
+const LOOT_PICKUP_RANGE = 5.0
+
+@onready var panel = $Panel
+@onready var gold_label = $Panel/VBoxContainer/GoldLabel
+@onready var item_list = $Panel/VBoxContainer/ScrollContainer/ItemList
+@onready var loot_all_button = $Panel/VBoxContainer/LootAllButton
+
+func _ready():
+ panel.visible = false
+ loot_all_button.pressed.connect(_on_loot_all)
+
+func setup(p):
+ player = p
+
+# Loot anzeigen
+func show_loot(loot: Dictionary, world_pos: Vector3):
+ current_loot = loot
+ loot_world_position = world_pos
+ _refresh_display()
+ panel_visible = true
+ panel.visible = true
+
+func hide_loot():
+ panel_visible = false
+ panel.visible = false
+ current_loot = {}
+
+func _refresh_display():
+ # Gold anzeigen
+ if current_loot.get("gold", 0) > 0:
+ gold_label.text = str(current_loot["gold"]) + " Gold"
+ gold_label.visible = true
+ else:
+ gold_label.visible = false
+
+ # Items anzeigen
+ for child in item_list.get_children():
+ child.queue_free()
+
+ var items = current_loot.get("items", [])
+ for i in range(items.size()):
+ var item = items[i]
+ var hbox = HBoxContainer.new()
+ hbox.add_theme_constant_override("separation", 4)
+
+ # Icon wenn vorhanden
+ if item.icon:
+ var icon = TextureRect.new()
+ icon.texture = item.icon
+ icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.custom_minimum_size = Vector2(24, 24)
+ hbox.add_child(icon)
+
+ var button = Button.new()
+ if item is Equipment:
+ button.text = item.item_name + " (" + Equipment.get_slot_name(item.slot) + ")"
+ button.modulate = Equipment.get_rarity_color(item.rarity)
+ elif item is Consumable:
+ var count_text = " x" + str(item.stack_size) if item.stack_size > 1 else ""
+ button.text = item.item_name + count_text
+ button.modulate = Color(0.3, 0.9, 0.3)
+ button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ button.pressed.connect(_on_loot_item.bind(i))
+ button.tooltip_text = _get_item_tooltip(item)
+ hbox.add_child(button)
+
+ item_list.add_child(hbox)
+
+ # Wenn kein Loot mehr da, Fenster schließen
+ if current_loot.get("gold", 0) <= 0 and items.size() == 0:
+ hide_loot()
+
+# Einzelnes Item aufheben
+func _on_loot_item(index: int):
+ if player == null:
+ return
+
+ var items = current_loot.get("items", [])
+ if index >= items.size():
+ return
+
+ var item = items[index]
+ if player.inventory.add_item(item):
+ items.remove_at(index)
+ _refresh_display()
+
+# Alles aufheben
+func _on_loot_all():
+ if player == null:
+ return
+
+ # Gold aufheben
+ var gold = current_loot.get("gold", 0)
+ if gold > 0:
+ player.inventory.add_gold(gold)
+ current_loot["gold"] = 0
+
+ # Items aufheben
+ var items = current_loot.get("items", [])
+ var remaining = []
+ for item in items:
+ if not player.inventory.add_item(item):
+ remaining.append(item) # Inventar voll
+ current_loot["items"] = remaining
+
+ _refresh_display()
+
+func _get_item_tooltip(item) -> String:
+ if item is Consumable:
+ return item.item_name + "\n" + item.get_effect_text()
+
+ var tooltip = item.item_name + "\n"
+ tooltip += Equipment.get_slot_name(item.slot) + "\n"
+
+ if item.slot == Equipment.Slot.WEAPON:
+ tooltip += "Schaden: " + str(item.min_damage) + "-" + str(item.max_damage) + "\n"
+ tooltip += "Tempo: " + str(item.attack_speed) + "s\n"
+ if item.armor > 0:
+ tooltip += "Rüstung: +" + str(item.armor) + "\n"
+ if item.strength > 0:
+ tooltip += "Stärke: +" + str(item.strength) + "\n"
+ if item.agility > 0:
+ tooltip += "Beweglichkeit: +" + str(item.agility) + "\n"
+ if item.intelligence > 0:
+ tooltip += "Intelligenz: +" + str(item.intelligence) + "\n"
+ if item.stamina > 0:
+ tooltip += "Ausdauer: +" + str(item.stamina) + "\n"
+ if item.haste > 0:
+ tooltip += "Tempo: +" + str(int(item.haste * 100)) + "%\n"
+
+ return tooltip
diff --git a/loot_window.gd.uid b/loot_window.gd.uid
new file mode 100644
index 0000000..478b980
--- /dev/null
+++ b/loot_window.gd.uid
@@ -0,0 +1 @@
+uid://y6v242l8n1ti
diff --git a/loot_window.tscn b/loot_window.tscn
new file mode 100644
index 0000000..569efd0
--- /dev/null
+++ b/loot_window.tscn
@@ -0,0 +1,61 @@
+[gd_scene format=3 uid="uid://loot_window"]
+
+[ext_resource type="Script" path="res://loot_window.gd" id="1_loot"]
+
+[node name="LootWindow" type="CanvasLayer"]
+script = ExtResource("1_loot")
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -130.0
+offset_top = -350.0
+offset_right = 130.0
+offset_bottom = -150.0
+grow_horizontal = 2
+grow_vertical = 0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -8.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 6
+
+[node name="Title" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Beute"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"]
+layout_mode = 2
+
+[node name="GoldLabel" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_colors/font_color = Color(1, 0.85, 0, 1)
+text = "0 Gold"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ItemList" type="VBoxContainer" parent="Panel/VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 4
+
+[node name="LootAllButton" type="Button" parent="Panel/VBoxContainer"]
+layout_mode = 2
+text = "Alles aufheben"
diff --git a/main_menu.gd b/main_menu.gd
new file mode 100644
index 0000000..4cf56b6
--- /dev/null
+++ b/main_menu.gd
@@ -0,0 +1,155 @@
+# MainMenu.gd
+# Hauptmenü mit Spielstart und Einstellungen
+extends CanvasLayer
+
+signal start_game
+
+@onready var main_panel = $MainPanel
+@onready var settings_panel = $SettingsPanel
+@onready var play_btn = $MainPanel/VBoxContainer/PlayBtn
+@onready var settings_btn = $MainPanel/VBoxContainer/SettingsBtn
+@onready var quit_btn = $MainPanel/VBoxContainer/QuitBtn
+
+# Settings Controls
+@onready var resolution_option = $SettingsPanel/VBoxContainer/ResolutionRow/ResolutionOption
+@onready var window_mode_option = $SettingsPanel/VBoxContainer/WindowModeRow/WindowModeOption
+@onready var vsync_check = $SettingsPanel/VBoxContainer/VsyncRow/VsyncCheck
+@onready var msaa_option = $SettingsPanel/VBoxContainer/MSAARow/MSAAOption
+@onready var back_btn = $SettingsPanel/VBoxContainer/BackBtn
+
+# Auflösungen
+const RESOLUTIONS = [
+ Vector2i(1280, 720),
+ Vector2i(1366, 768),
+ Vector2i(1600, 900),
+ Vector2i(1920, 1080),
+ Vector2i(2560, 1440),
+ Vector2i(3840, 2160),
+]
+
+func _ready():
+ get_tree().paused = true
+ process_mode = Node.PROCESS_MODE_ALWAYS
+
+ main_panel.visible = true
+ settings_panel.visible = false
+
+ play_btn.pressed.connect(_on_play)
+ settings_btn.pressed.connect(_on_settings)
+ quit_btn.pressed.connect(_on_quit)
+ back_btn.pressed.connect(_on_back)
+
+ _setup_resolution_options()
+ _setup_window_mode_options()
+ _setup_vsync()
+ _setup_msaa()
+
+func _setup_resolution_options():
+ resolution_option.clear()
+ var current_size = DisplayServer.window_get_size()
+ var selected = 0
+ for i in range(RESOLUTIONS.size()):
+ var res = RESOLUTIONS[i]
+ resolution_option.add_item(str(res.x) + " x " + str(res.y), i)
+ if res == current_size:
+ selected = i
+ resolution_option.selected = selected
+ resolution_option.item_selected.connect(_on_resolution_changed)
+
+func _setup_window_mode_options():
+ window_mode_option.clear()
+ window_mode_option.add_item("Fenster", 0)
+ window_mode_option.add_item("Randloses Fenster", 1)
+ window_mode_option.add_item("Vollbild", 2)
+
+ var current_mode = DisplayServer.window_get_mode()
+ match current_mode:
+ DisplayServer.WINDOW_MODE_WINDOWED:
+ window_mode_option.selected = 0
+ DisplayServer.WINDOW_MODE_FULLSCREEN:
+ window_mode_option.selected = 2
+ _:
+ window_mode_option.selected = 0
+
+ window_mode_option.item_selected.connect(_on_window_mode_changed)
+
+func _setup_vsync():
+ var vsync_mode = DisplayServer.window_get_vsync_mode()
+ vsync_check.button_pressed = (vsync_mode != DisplayServer.VSYNC_DISABLED)
+ vsync_check.toggled.connect(_on_vsync_toggled)
+
+func _setup_msaa():
+ msaa_option.clear()
+ msaa_option.add_item("Aus", 0)
+ msaa_option.add_item("2x", 1)
+ msaa_option.add_item("4x", 2)
+ msaa_option.add_item("8x", 3)
+
+ var current_msaa = get_viewport().msaa_3d
+ match current_msaa:
+ Viewport.MSAA_DISABLED:
+ msaa_option.selected = 0
+ Viewport.MSAA_2X:
+ msaa_option.selected = 1
+ Viewport.MSAA_4X:
+ msaa_option.selected = 2
+ Viewport.MSAA_8X:
+ msaa_option.selected = 3
+
+ msaa_option.item_selected.connect(_on_msaa_changed)
+
+func _on_resolution_changed(index: int):
+ var res = RESOLUTIONS[index]
+ DisplayServer.window_set_size(res)
+ # Fenster zentrieren
+ var screen_size = DisplayServer.screen_get_size()
+ var window_pos = (screen_size - res) / 2
+ DisplayServer.window_set_position(window_pos)
+
+func _on_window_mode_changed(index: int):
+ match index:
+ 0: # Fenster
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
+ DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
+ 1: # Randloses Fenster
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
+ DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
+ var screen_size = DisplayServer.screen_get_size()
+ DisplayServer.window_set_size(screen_size)
+ DisplayServer.window_set_position(Vector2i.ZERO)
+ 2: # Vollbild
+ DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
+
+func _on_vsync_toggled(enabled: bool):
+ if enabled:
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
+ else:
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
+
+func _on_msaa_changed(index: int):
+ match index:
+ 0:
+ get_viewport().msaa_3d = Viewport.MSAA_DISABLED
+ 1:
+ get_viewport().msaa_3d = Viewport.MSAA_2X
+ 2:
+ get_viewport().msaa_3d = Viewport.MSAA_4X
+ 3:
+ get_viewport().msaa_3d = Viewport.MSAA_8X
+
+func _on_play():
+ start_game.emit()
+ get_tree().paused = false
+ queue_free()
+
+func _on_settings():
+ main_panel.visible = false
+ settings_panel.visible = true
+
+func _on_back():
+ settings_panel.visible = false
+ main_panel.visible = true
+
+func _on_quit():
+ get_tree().quit()
diff --git a/main_menu.gd.uid b/main_menu.gd.uid
new file mode 100644
index 0000000..6c0ecdd
--- /dev/null
+++ b/main_menu.gd.uid
@@ -0,0 +1 @@
+uid://bw65mud8idif0
diff --git a/main_menu.tscn b/main_menu.tscn
new file mode 100644
index 0000000..f9048c2
--- /dev/null
+++ b/main_menu.tscn
@@ -0,0 +1,161 @@
+[gd_scene format=3 uid="uid://main_menu"]
+
+[ext_resource type="Script" path="res://main_menu.gd" id="1_menu"]
+
+[node name="MainMenu" type="CanvasLayer"]
+script = ExtResource("1_menu")
+
+[node name="MainPanel" type="Panel" parent="."]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -180.0
+offset_right = 200.0
+offset_bottom = 180.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MainPanel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 30.0
+offset_top = 30.0
+offset_right = -30.0
+offset_bottom = -30.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 20
+
+[node name="Title" type="Label" parent="MainPanel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 32
+text = "DungeonCrawler"
+horizontal_alignment = 1
+
+[node name="Spacer" type="Control" parent="MainPanel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="PlayBtn" type="Button" parent="MainPanel/VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+text = "Spielen"
+
+[node name="SettingsBtn" type="Button" parent="MainPanel/VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+text = "Einstellungen"
+
+[node name="QuitBtn" type="Button" parent="MainPanel/VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+theme_override_font_sizes/font_size = 20
+text = "Beenden"
+
+[node name="SettingsPanel" type="Panel" parent="."]
+visible = false
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -250.0
+offset_top = -220.0
+offset_right = 250.0
+offset_bottom = 220.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 20.0
+offset_top = 20.0
+offset_right = -20.0
+offset_bottom = -20.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 15
+
+[node name="Title" type="Label" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 24
+text = "Einstellungen"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="ResolutionRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="SettingsPanel/VBoxContainer/ResolutionRow"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Auflösung"
+
+[node name="ResolutionOption" type="OptionButton" parent="SettingsPanel/VBoxContainer/ResolutionRow"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+
+[node name="WindowModeRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="SettingsPanel/VBoxContainer/WindowModeRow"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Fenstermodus"
+
+[node name="WindowModeOption" type="OptionButton" parent="SettingsPanel/VBoxContainer/WindowModeRow"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+
+[node name="VsyncRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="SettingsPanel/VBoxContainer/VsyncRow"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "VSync"
+
+[node name="VsyncCheck" type="CheckBox" parent="SettingsPanel/VBoxContainer/VsyncRow"]
+layout_mode = 2
+text = "Aktiviert"
+
+[node name="MSAARow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="SettingsPanel/VBoxContainer/MSAARow"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Anti-Aliasing"
+
+[node name="MSAAOption" type="OptionButton" parent="SettingsPanel/VBoxContainer/MSAARow"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+
+[node name="Spacer" type="Control" parent="SettingsPanel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="BackBtn" type="Button" parent="SettingsPanel/VBoxContainer"]
+custom_minimum_size = Vector2(0, 45)
+layout_mode = 2
+theme_override_font_sizes/font_size = 18
+text = "Zurück"
diff --git a/player.gd b/player.gd
index 4ae3bed..ed457bf 100644
--- a/player.gd
+++ b/player.gd
@@ -6,52 +6,724 @@ const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
+# Charakter-Klasse und Level-System
+@export var character_class: CharacterClass
+var level: int = 1
+var current_xp: int = 0
+var xp_to_next_level: int = 100 # XP benötigt für Level 2
+
+# Aktuelle Stats (berechnet aus Klasse + Level)
+var strength: int = 10
+var agility: int = 10
+var intelligence: int = 10
+var stamina: int = 10
+var armor: int = 0 # Rüstung aus Ausrüstung
+
+# Level-Differenz Konstanten
+const LEVEL_DIFF_DAMAGE_MOD = 0.1 # 10% mehr/weniger Schaden pro Level-Differenz
+const MAX_LEVEL_DIFF_MOD = 0.5 # Maximal 50% Modifikation
+
var max_hp = 100
var current_hp = 100
-var can_attack = true
+var max_resource = 0 # Klassen-Ressource (Mana/Energie/Wut), 0 = keine
+var current_resource = 0
var target = null # Aktuell markierter Gegner
-var equipped_weapon = null # Ausgerüstete Waffe (null = unbewaffnet, Schaden = 1)
+
+# Aktionsleiste: Skills (String) oder Consumables in Slots (0-8)
+var action_bar_items: Array = [null, null, null, null, null, null, null, null, null]
+
+# Alle verfügbaren Skills (für Fähigkeiten-Panel) — wird klassenabhängig befüllt
+var available_skills: Array = []
+
+# Skill-Definitionen pro Klasse
+const AUTOATTACK_SKILL = {"id": "autoattack", "name": "Autoattack", "icon": "res://icons/autoattack_icon.svg", "description": "Greift das Ziel im Nahkampf an.\nSchaden: Waffenschaden + Main-Stat"}
+const MELEE_SKILLS = [
+ {"id": "heavy_strike", "name": "Heavy Strike", "icon": "res://icons/heavy_strike_icon.svg", "description": "Starker Hieb mit 3s Cooldown.\nSchaden: 10-15 + Main-Stat\nReichweite: 4.0"},
+]
+const MAGE_SKILLS = [
+ {"id": "wand", "name": "Zauberstab", "icon": "res://icons/wand_icon.svg", "description": "Magischer Fernkampfangriff.\nSchaden: Waffenschaden + INT\nReichweite: 20.0\nIgnoriert Rüstung\nDeaktiviert Autoattack"},
+ {"id": "frostbolt", "name": "Frostblitz", "icon": "res://icons/frostbolt_icon.svg", "description": "Magischer Fernkampfangriff mit Castzeit.\nSchaden: 12-20 + INT\nManakosten: 20\nReichweite: 20.0\nCastzeit: 1.5s\nCooldown: 2.5s"},
+]
+var potion_cooldown: float = 0.0
+const POTION_COOLDOWN_TIME = 1.0
+
+# Equipment System
+var equipment: Dictionary = {
+ Equipment.Slot.HEAD: null,
+ Equipment.Slot.CHEST: null,
+ Equipment.Slot.HANDS: null,
+ Equipment.Slot.LEGS: null,
+ Equipment.Slot.FEET: null,
+ Equipment.Slot.WEAPON: null,
+ Equipment.Slot.OFFHAND: null
+}
+
+# Inventar System
+var inventory: Inventory = Inventory.new()
+
+# Global Cooldown System (GCD) - gilt für alle Aktionen inkl. Autoattack
+var global_cooldown = 0.0
+const BASE_GCD = 1.5 # Basis-GCD in Sekunden (wird durch Haste modifiziert)
+var haste: float = 0.0 # Angriffsgeschwindigkeits-Bonus (0.1 = 10% schneller)
+
+# Autoattack System
+var autoattack_active = false # Ob Autoattack aktiv ist
+
+# Zauberstab System (Magier-Fernkampf, exklusiv mit Autoattack)
+var wand_active = false
+const WAND_RANGE = 20.0
+
+# Skills System - individuelle Cooldowns (zusätzlich zum GCD)
+var heavy_strike_cooldown = 0.0
+const HEAVY_STRIKE_DAMAGE_MIN = 10
+const HEAVY_STRIKE_DAMAGE_MAX = 15
+const HEAVY_STRIKE_COOLDOWN = 3.0
+const HEAVY_STRIKE_RANGE = 4.0
+
+var frostbolt_cooldown = 0.0
+const FROSTBOLT_DAMAGE_MIN = 12
+const FROSTBOLT_DAMAGE_MAX = 20
+const FROSTBOLT_COOLDOWN = 2.5
+const FROSTBOLT_RANGE = 20.0
+const FROSTBOLT_MANA_COST = 20
+const FROSTBOLT_CAST_TIME = 1.5
+
+# Cast-System
+var is_casting = false
+var cast_time_remaining = 0.0
+var cast_time_total = 0.0
+var cast_spell_id = "" # Welcher Zauber gecastet wird
+
+# Animation System
+const ANIMATION_FILES = {
+ "start_walk": "res://assets/animations/Start Walking.fbx",
+ "walk": "res://assets/animations/Walking.fbx",
+ "stop_walk": "res://assets/animations/Stop Walking.fbx",
+ "walk_back": "res://assets/animations/Walking Backwards.fbx",
+ "strafe_left": "res://assets/animations/Left Strafe Walking.fbx",
+ "strafe_right": "res://assets/animations/Right Strafe Walking.fbx",
+ "jump": "res://assets/animations/Jumping.fbx",
+ "autoattack": "res://assets/animations/Autoattack.fbx",
+ "heavy_strike": "res://assets/animations/Heavy Strike.fbx",
+ "die": "res://assets/animations/Dying Backwards.fbx",
+ "idle": "res://assets/animations/Idle.fbx",
+}
+# Animations-State für Walk-Kette
+var walk_state: String = "" # "", "start", "walking", "stop"
+var anim_player: AnimationPlayer = null
+var current_anim: String = ""
@onready var camera_pivot = $CameraPivot
@onready var camera = $CameraPivot/Camera3D
@onready var hud = $HUD
+@onready var character_panel = $CharacterPanel
+@onready var inventory_panel = $InventoryPanel
+@onready var loot_window = $LootWindow
+@onready var skill_panel = $SkillPanel
func _ready():
+ # Stats aus Klasse berechnen
+ _calculate_stats()
+ current_hp = max_hp
+ current_resource = max_resource
+
hud.update_health(current_hp, max_hp)
+ hud.update_resource(current_resource, max_resource, get_resource_name())
+ hud.update_level(level, current_xp, xp_to_next_level)
hud.set_active_slot(0)
+ # Skills klassenabhängig aufbauen
+ _init_class_skills()
+ # Aktionsleiste initialisieren (Skills + Items)
+ for i in range(9):
+ _refresh_action_slot(i)
+
+ # HUD-Klicks und Drag verbinden
+ hud.slot_clicked.connect(_on_slot_clicked)
+ hud.slot_drag_removed.connect(_on_slot_drag_removed)
+ hud.slot_drag_swapped.connect(_on_slot_drag_swapped)
+
+ # Inventar Panel initialisieren
+ inventory_panel.setup(self)
+
+ # Loot Window initialisieren
+ loot_window.setup(self)
+
+ # Skill Panel initialisieren
+ skill_panel.setup(self)
+
+ # Gold im HUD aktualisieren wenn sich Gold ändert
+ inventory.gold_changed.connect(func(amount): hud.update_gold(amount))
+
+ # Animationen laden
+ _setup_animations()
+
+# Animationen aus FBX-Dateien laden und in AnimationPlayer einbinden
+func _setup_animations():
+ var model = get_node_or_null("PlayerModel")
+ if model == null:
+ return
+
+ # AnimationPlayer im Modell finden
+ anim_player = _find_node_by_class(model, "AnimationPlayer")
+ if anim_player == null:
+ anim_player = AnimationPlayer.new()
+ anim_player.name = "AnimationPlayer"
+ model.add_child(anim_player)
+
+ # AnimationLibrary holen oder erstellen
+ var lib: AnimationLibrary
+ if anim_player.has_animation_library(""):
+ lib = anim_player.get_animation_library("")
+ else:
+ lib = AnimationLibrary.new()
+ anim_player.add_animation_library("", lib)
+
+ # Animationen aus separaten FBX-Dateien laden
+ for anim_id in ANIMATION_FILES:
+ var scene = load(ANIMATION_FILES[anim_id]) as PackedScene
+ if scene == null:
+ continue
+ var instance = scene.instantiate()
+ var source_ap = _find_node_by_class(instance, "AnimationPlayer")
+ if source_ap:
+ var names = source_ap.get_animation_list()
+ if names.size() > 0:
+ var anim = source_ap.get_animation(names[0])
+ # Endlos-Animationen loopen
+ if anim_id in ["walk", "walk_back", "strafe_left", "strafe_right", "idle"]:
+ anim.loop_mode = Animation.LOOP_LINEAR
+ if lib.has_animation(anim_id):
+ lib.remove_animation(anim_id)
+ lib.add_animation(anim_id, anim)
+ instance.queue_free()
+
+ # Signal für Walk-Kette: Start Walking → Walking
+ anim_player.animation_finished.connect(_on_animation_finished)
+
+# Callback wenn eine Animation fertig ist
+func _on_animation_finished(anim_name: StringName):
+ if anim_name == "start_walk" and walk_state == "start":
+ # Start Walking fertig → Walking (loop) starten
+ walk_state = "walking"
+ current_anim = "walk"
+ anim_player.play("walk")
+ elif anim_name == "stop_walk" and walk_state == "stop":
+ # Stop Walking fertig → Idle
+ walk_state = ""
+ current_anim = "idle"
+ anim_player.play("idle")
+
+# Rekursiv nach einem Node einer bestimmten Klasse suchen
+func _find_node_by_class(node: Node, class_name_str: String) -> Node:
+ for child in node.get_children():
+ if child.get_class() == class_name_str:
+ return child
+ var result = _find_node_by_class(child, class_name_str)
+ if result:
+ return result
+ return null
+
+# Animation basierend auf Bewegungszustand abspielen
+func _update_animation(input_dir: Vector2):
+ if anim_player == null:
+ return
+
+ # Angriffs-/Death-Animation nicht unterbrechen
+ if anim_player.is_playing() and current_anim in ["autoattack", "heavy_strike", "die"]:
+ return
+
+ var is_walking_forward = input_dir.y < -0.1 or (input_dir.length() > 0 and abs(input_dir.y) <= 0.1 and abs(input_dir.x) <= 0.1)
+ var is_moving = input_dir.length() > 0
+
+ # Vorwärts-Laufen: Start Walking → Walking → Stop Walking
+ if is_walking_forward:
+ if walk_state == "":
+ # Loslaufen: Start Walking abspielen
+ walk_state = "start"
+ current_anim = "start_walk"
+ anim_player.play("start_walk")
+ elif walk_state == "start" and not anim_player.is_playing():
+ # Start Walking fertig → Walking (loop)
+ walk_state = "walking"
+ current_anim = "walk"
+ anim_player.play("walk")
+ elif walk_state == "stop":
+ # War gerade am Anhalten, wieder loslaufen
+ walk_state = "start"
+ current_anim = "start_walk"
+ anim_player.play("start_walk")
+ # walking state: walk loopt automatisch
+ return
+
+ # Nicht mehr vorwärts → Stop Walking wenn nötig
+ if walk_state == "start" or walk_state == "walking":
+ if not is_moving:
+ walk_state = "stop"
+ current_anim = "stop_walk"
+ anim_player.play("stop_walk")
+ return
+ else:
+ # Wechsel zu anderer Bewegung (rückwärts, strafe)
+ walk_state = ""
+
+ # Stop Walking läuft noch
+ if walk_state == "stop":
+ if anim_player.is_playing():
+ return # Warten bis Stop Walking fertig
+ walk_state = ""
+
+ # Andere Animationen
+ var new_anim = ""
+ if not is_on_floor():
+ new_anim = "jump"
+ elif is_moving:
+ if input_dir.y > 0.1:
+ new_anim = "walk_back"
+ elif input_dir.x < -0.1:
+ new_anim = "strafe_left"
+ elif input_dir.x > 0.1:
+ new_anim = "strafe_right"
+ else:
+ new_anim = "walk"
+ else:
+ new_anim = "idle"
+
+ # Wenn Animation ausgelaufen ist, zurücksetzen
+ if not anim_player.is_playing():
+ current_anim = ""
+
+ if new_anim != current_anim:
+ current_anim = new_anim
+ if anim_player.has_animation(new_anim):
+ anim_player.play(new_anim)
+ else:
+ anim_player.stop()
+
+# Einmalige Animation abspielen (Angriff, Tod, etc.)
+func _play_attack_anim(anim_name: String):
+ if anim_player == null:
+ return
+ if anim_player.has_animation(anim_name):
+ current_anim = anim_name
+ walk_state = "" # Walk-Kette unterbrechen
+ anim_player.stop()
+ anim_player.play(anim_name)
+
+# Stats basierend auf Klasse und Level berechnen
+func _calculate_stats():
+ if character_class == null:
+ # Fallback ohne Klasse
+ strength = 10
+ agility = 10
+ intelligence = 10
+ stamina = 10
+ max_hp = 100
+ max_resource = 0
+ return
+
+ # Stats = Basis + (Level-1) * Zuwachs pro Level
+ var levels_gained = level - 1
+ strength = character_class.base_strength + int(levels_gained * character_class.strength_per_level)
+ agility = character_class.base_agility + int(levels_gained * character_class.agility_per_level)
+ intelligence = character_class.base_intelligence + int(levels_gained * character_class.intelligence_per_level)
+ stamina = character_class.base_stamina + int(levels_gained * character_class.stamina_per_level)
+
+ # HP aus Stamina berechnen
+ max_hp = stamina * CharacterClass.HP_PER_STAMINA
+ # Klassen-Ressource berechnen
+ _calculate_resource()
+
+ # Equipment-Boni hinzufügen
+ _apply_equipment_stats()
+
+ print("Stats berechnet - STR: ", strength, " AGI: ", agility, " INT: ", intelligence, " STA: ", stamina, " ARM: ", armor, " HP: ", max_hp, " RES: ", max_resource)
+
+# Skills klassenabhängig aufbauen
+func _init_class_skills():
+ available_skills = [AUTOATTACK_SKILL.duplicate()]
+ if character_class and character_class.resource_type == CharacterClass.ResourceType.MANA:
+ # Magier: Autoattack + Zauberstab + Frostblitz
+ available_skills.append_array(MAGE_SKILLS.duplicate(true))
+ action_bar_items[0] = "wand"
+ action_bar_items[1] = "frostbolt"
+ action_bar_items[2] = "autoattack"
+ else:
+ # Krieger/Schurke: Autoattack + Heavy Strike
+ available_skills.append_array(MELEE_SKILLS.duplicate(true))
+ action_bar_items[0] = "autoattack"
+ action_bar_items[1] = "heavy_strike"
+
+# Klassen-Ressource berechnen (Mana aus INT, Energie fix, Wut fix)
+func _calculate_resource():
+ if character_class == null or character_class.resource_type == CharacterClass.ResourceType.NONE:
+ max_resource = 0
+ return
+ match character_class.resource_type:
+ CharacterClass.ResourceType.MANA:
+ max_resource = character_class.base_resource + intelligence * CharacterClass.MANA_PER_INT
+ CharacterClass.ResourceType.ENERGY:
+ max_resource = character_class.base_resource # Fix, skaliert nicht
+ CharacterClass.ResourceType.RAGE:
+ max_resource = character_class.base_resource # Fix, skaliert nicht
+
+# Name der Klassen-Ressource
+func get_resource_name() -> String:
+ if character_class == null:
+ return ""
+ match character_class.resource_type:
+ CharacterClass.ResourceType.MANA: return "Mana"
+ CharacterClass.ResourceType.ENERGY: return "Energie"
+ CharacterClass.ResourceType.RAGE: return "Wut"
+ return ""
+
+# Equipment-Stats auf Charakter anwenden
+func _apply_equipment_stats():
+ armor = 0
+ haste = 0.0
+ var bonus_str = 0
+ var bonus_agi = 0
+ var bonus_int = 0
+ var bonus_sta = 0
+
+ for slot in equipment.keys():
+ var item = equipment[slot]
+ if item != null:
+ armor += item.armor
+ haste += item.haste
+ bonus_str += item.strength
+ bonus_agi += item.agility
+ bonus_int += item.intelligence
+ bonus_sta += item.stamina
+
+ strength += bonus_str
+ agility += bonus_agi
+ intelligence += bonus_int
+ stamina += bonus_sta
+
+ # HP und Ressource neu berechnen mit Equipment-Boni
+ max_hp = stamina * CharacterClass.HP_PER_STAMINA
+ _calculate_resource()
+
+# Equipment anlegen
+func equip_item(item: Equipment) -> Equipment:
+ var old_item = equipment[item.slot]
+ equipment[item.slot] = item
+ _calculate_stats()
+ # HP proportional anpassen
+ if max_hp > 0:
+ current_hp = mini(current_hp, max_hp)
+ hud.update_health(current_hp, max_hp)
+ character_panel.update_stats(self)
+ print("Ausgerüstet: ", item.item_name, " in Slot ", Equipment.get_slot_name(item.slot))
+ return old_item
+
+# Equipment ablegen
+func unequip_slot(slot: Equipment.Slot) -> Equipment:
+ var old_item = equipment[slot]
+ if old_item == null:
+ return null
+ equipment[slot] = null
+ _calculate_stats()
+ current_hp = mini(current_hp, max_hp)
+ hud.update_health(current_hp, max_hp)
+ character_panel.update_stats(self)
+ print("Abgelegt: ", old_item.item_name)
+ return old_item
+
+# Ausgerüstete Waffe holen
+func get_equipped_weapon() -> Equipment:
+ return equipment[Equipment.Slot.WEAPON]
+
+# Main-Stat für Schadensberechnung holen
+func get_main_stat() -> int:
+ if character_class == null:
+ return 10
+ match character_class.main_stat:
+ CharacterClass.MainStat.STRENGTH:
+ return strength
+ CharacterClass.MainStat.AGILITY:
+ return agility
+ CharacterClass.MainStat.INTELLIGENCE:
+ return intelligence
+ return 10
+
+# XP erhalten und Level-Up prüfen
+func gain_xp(amount: int):
+ current_xp += amount
+ print("+" , amount, " XP (", current_xp, "/", xp_to_next_level, ")")
+
+ while current_xp >= xp_to_next_level:
+ _level_up()
+
+ hud.update_level(level, current_xp, xp_to_next_level)
+
+# Level-Up durchführen
+func _level_up():
+ current_xp -= xp_to_next_level
+ level += 1
+ xp_to_next_level = _calculate_xp_for_level(level + 1)
+
+ # Stats neu berechnen
+ _calculate_stats()
+
+ # HP und Ressource vollständig auffüllen bei Level-Up
+ current_hp = max_hp
+ current_resource = max_resource
+
+ hud.update_health(current_hp, max_hp)
+ hud.update_resource(current_resource, max_resource, get_resource_name())
+ # Character Panel aktualisieren falls offen
+ character_panel.update_stats(self)
+ print("LEVEL UP! Jetzt Level ", level, " - HP und Ressource voll aufgefüllt!")
+
+# XP-Kurve: Jedes Level braucht mehr XP
+func _calculate_xp_for_level(target_level: int) -> int:
+ return 100 * target_level # Level 2: 100, Level 3: 200, etc.
+
+# Handler für HUD-Slot-Klicks
+func _on_slot_clicked(slot_index: int):
+ _use_action_slot(slot_index)
+
+# Slot aus Aktionsleiste entfernen (rausgezogen) - Item/Skill bleibt verfügbar
+func _on_slot_drag_removed(slot_index: int):
+ action_bar_items[slot_index] = null
+ hud.clear_slot_icon(slot_index)
+ hud.set_slot_stack_count(slot_index, 0)
+ print("Slot " + str(slot_index + 1) + " geleert")
+
+# Zwei Slots tauschen
+func _on_slot_drag_swapped(from_slot: int, to_slot: int):
+ var temp = action_bar_items[from_slot]
+ action_bar_items[from_slot] = action_bar_items[to_slot]
+ action_bar_items[to_slot] = temp
+ _refresh_action_slot(from_slot)
+ _refresh_action_slot(to_slot)
+
+# Skill per ID auf Slot legen
+func assign_skill_to_action_bar(slot_index: int, skill_id: String):
+ action_bar_items[slot_index] = skill_id
+ _refresh_action_slot(slot_index)
+ print(skill_id + " auf Slot " + str(slot_index + 1) + " gelegt")
+
+# Skill-Info anhand ID holen
+func get_skill_info(skill_id: String) -> Dictionary:
+ for skill in available_skills:
+ if skill["id"] == skill_id:
+ return skill
+ return {}
+
+# Cooldown für einen Slot ermitteln
+func _get_slot_cooldown(slot_index: int) -> float:
+ var entry = action_bar_items[slot_index]
+ if entry is String:
+ match entry:
+ "autoattack":
+ return global_cooldown
+ "wand":
+ return global_cooldown
+ "heavy_strike":
+ return heavy_strike_cooldown
+ "frostbolt":
+ return frostbolt_cooldown
+ elif entry is Consumable:
+ return potion_cooldown
+ return 0.0
+
+func _refresh_action_slot(slot_index: int):
+ var entry = action_bar_items[slot_index]
+ if entry is String:
+ # Skill
+ var info = get_skill_info(entry)
+ if info.size() > 0:
+ hud.set_slot_icon(slot_index, info["icon"])
+ else:
+ hud.clear_slot_icon(slot_index)
+ hud.set_slot_stack_count(slot_index, 0)
+ elif entry is Consumable and entry.icon:
+ hud.set_slot_icon_texture(slot_index, entry.icon)
+ hud.set_slot_stack_count(slot_index, entry.stack_size)
+ else:
+ hud.clear_slot_icon(slot_index)
+ hud.set_slot_stack_count(slot_index, 0)
+
+func _use_action_slot(slot_index: int):
+ var entry = action_bar_items[slot_index]
+ if entry is String:
+ # Skill ausführen
+ match entry:
+ "autoattack":
+ if target != null and global_cooldown <= 0:
+ wand_active = false # Zauberstab deaktivieren
+ start_autoattack()
+ perform_autoattack()
+ "wand":
+ if target != null and global_cooldown <= 0:
+ autoattack_active = false # Autoattack deaktivieren
+ start_wand()
+ perform_wand_attack()
+ "heavy_strike":
+ use_heavy_strike()
+ "frostbolt":
+ use_frostbolt()
+ elif entry is Consumable:
+ if use_consumable(entry):
+ if entry.stack_size <= 0:
+ inventory.remove_item(entry)
+ _update_action_bar_stacks()
# Schaden am Spieler abziehen und HP-Leiste aktualisieren
func take_damage(amount):
current_hp = clamp(current_hp - amount, 0, max_hp)
hud.update_health(current_hp, max_hp)
+ if is_casting:
+ _cancel_cast()
if current_hp <= 0:
die()
+# Schaden mit Rüstung und Level-Differenz berechnen
+func calculate_incoming_damage(raw_damage: int, attacker_level: int, is_melee: bool) -> int:
+ var damage = float(raw_damage)
+
+ # Rüstung reduziert nur Nahkampfschaden
+ if is_melee and armor > 0:
+ var armor_reduction = float(armor) / (float(armor) + 50.0)
+ damage = damage * (1.0 - armor_reduction)
+
+ # Level-Differenz Modifikator (Gegner höheres Level = mehr Schaden)
+ var level_diff = attacker_level - level
+ var level_mod = clamp(level_diff * LEVEL_DIFF_DAMAGE_MOD, -MAX_LEVEL_DIFF_MOD, MAX_LEVEL_DIFF_MOD)
+ damage = damage * (1.0 + level_mod)
+
+ return maxi(1, int(damage))
+
+# Schaden mit vollem Schadenssystem nehmen
+func take_damage_from(raw_damage: int, attacker_level: int, is_melee: bool = true):
+ var final_damage = calculate_incoming_damage(raw_damage, attacker_level, is_melee)
+ print("Spieler nimmt Schaden: ", raw_damage, " -> ", final_damage, " (nach Rüstung/Level)")
+ take_damage(final_damage)
+
# HP heilen und HP-Leiste aktualisieren
func heal(amount):
current_hp = clamp(current_hp + amount, 0, max_hp)
hud.update_health(current_hp, max_hp)
+# Ressource wiederherstellen (Mana/Energie/Wut)
+func restore_mana(amount):
+ current_resource = clamp(current_resource + amount, 0, max_resource)
+ hud.update_resource(current_resource, max_resource, get_resource_name())
+
+# Ressource verbrauchen
+func spend_resource(amount) -> bool:
+ if current_resource < amount:
+ print("Nicht genug " + get_resource_name() + "!")
+ return false
+ current_resource = clamp(current_resource - amount, 0, max_resource)
+ hud.update_resource(current_resource, max_resource, get_resource_name())
+ return true
+
+# Consumable benutzen (Trank etc.)
+func use_consumable(consumable: Consumable) -> bool:
+ if potion_cooldown > 0:
+ print("Trank noch im Cooldown!")
+ return false
+ if consumable.use(self):
+ potion_cooldown = consumable.cooldown
+ if consumable.stack_size <= 0:
+ return true # Verbraucht
+ return false
+
+# Consumable auf Aktionsleiste legen
+func assign_to_action_bar(slot_index: int, consumable: Consumable):
+ if slot_index < 2 or slot_index > 8:
+ return # Slot 0+1 sind reserviert für Skills
+ action_bar_items[slot_index] = consumable
+ if consumable and consumable.icon:
+ hud.set_slot_icon_texture(slot_index, consumable.icon)
+ hud.set_slot_stack_count(slot_index, consumable.stack_size)
+ else:
+ hud.clear_slot_icon(slot_index)
+ hud.set_slot_stack_count(slot_index, 0)
+
+# Aktionsleiste Stack-Counts aktualisieren
+func _update_action_bar_stacks():
+ for i in range(2, 9):
+ var item = action_bar_items[i]
+ if item is Consumable:
+ if item.stack_size <= 0:
+ action_bar_items[i] = null
+ hud.clear_slot_icon(i)
+ hud.set_slot_stack_count(i, 0)
+ else:
+ hud.set_slot_stack_count(i, item.stack_size)
+
+# Loot empfangen und Fenster anzeigen
+func receive_loot(loot: Dictionary, world_pos: Vector3):
+ loot_window.show_loot(loot, world_pos)
+
func die():
print("Spieler gestorben!")
+ _play_attack_anim("die")
-# Schaden basierend auf ausgerüsteter Waffe (unbewaffnet = 1)
+# Schaden basierend auf ausgerüsteter Waffe + Main-Stat Skalierung
func get_attack_damage() -> int:
- if equipped_weapon == null:
- return 1
- return randi_range(equipped_weapon.min_damage, equipped_weapon.max_damage)
+ var weapon = get_equipped_weapon()
+ var base_damage: int
+ if weapon == null:
+ # Unbewaffneter Schaden klassenabhängig
+ if character_class:
+ base_damage = randi_range(character_class.unarmed_min_damage, character_class.unarmed_max_damage)
+ else:
+ base_damage = 1
+ else:
+ base_damage = randi_range(weapon.min_damage, weapon.max_damage)
-# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5)
+ # Schaden skaliert mit Main-Stat
+ var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
+ return base_damage + stat_bonus
+
+# Aktuellen GCD berechnen (mit Haste-Modifikator)
+func get_current_gcd() -> float:
+ var weapon = get_equipped_weapon()
+ var base_speed: float
+ if weapon == null:
+ # Unbewaffnete Angriffsgeschwindigkeit klassenabhängig
+ if character_class:
+ base_speed = character_class.unarmed_attack_speed
+ else:
+ base_speed = BASE_GCD
+ else:
+ base_speed = weapon.attack_speed
+
+ # Haste reduziert den GCD: GCD = Basis / (1 + Haste)
+ # Bei 0.5 Haste (50%): 1.5s / 1.5 = 1.0s
+ return base_speed / (1.0 + haste)
+
+# DPS berechnen (für Anzeige)
+func get_dps() -> float:
+ var weapon = get_equipped_weapon()
+ var avg_damage: float
+ if weapon == null:
+ # Unbewaffneter Durchschnittsschaden klassenabhängig
+ if character_class:
+ avg_damage = (character_class.unarmed_min_damage + character_class.unarmed_max_damage) / 2.0
+ else:
+ avg_damage = 1.0
+ else:
+ avg_damage = (weapon.min_damage + weapon.max_damage) / 2.0
+
+ var stat_bonus = get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT
+ var total_damage = avg_damage + stat_bonus
+ var gcd = get_current_gcd()
+
+ # DPS = Schaden / GCD
+ return total_damage / gcd
+
+# Reichweite basierend auf ausgerüsteter Waffe (Nahkampf)
func get_attack_range() -> float:
- if equipped_weapon == null:
- return 1.5
- return equipped_weapon.range
-
-# Angriffsgeschwindigkeit basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5s)
-func get_attack_cooldown() -> float:
- if equipped_weapon == null:
- return 1.5
- return equipped_weapon.attack_speed
+ var weapon = get_equipped_weapon()
+ if weapon == null:
+ return 3.0
+ return weapon.weapon_range
# Ziel markieren — start_attack=true startet sofort die Autoattack
func set_target(new_target, start_attack: bool = false):
@@ -60,29 +732,195 @@ func set_target(new_target, start_attack: bool = false):
target = new_target
target.show_health()
print("Ziel markiert: ", target.name)
- if start_attack and can_attack:
- autoattack()
+ if start_attack:
+ _start_default_attack()
-# Autoattack: greift wiederholt an solange Ziel gültig ist
-func autoattack():
+# Ziel komplett aufheben und Autoattack stoppen
+func clear_target():
+ if target != null and is_instance_valid(target):
+ target.hide_health()
+ target = null
+ autoattack_active = false
+ wand_active = false
+ print("Ziel aufgehoben, Angriff gestoppt")
+
+# Standard-Angriff starten (Rechtsklick): Magier=Zauberstab, Rest=Autoattack
+func _start_default_attack():
+ if character_class and character_class.resource_type == CharacterClass.ResourceType.MANA:
+ start_wand()
+ if global_cooldown <= 0:
+ perform_wand_attack()
+ else:
+ start_autoattack()
+ if global_cooldown <= 0:
+ perform_autoattack()
+
+# Autoattack aktivieren
+func start_autoattack():
+ autoattack_active = true
+ print("Autoattack aktiviert")
+
+# Autoattack deaktivieren
+func stop_autoattack():
+ autoattack_active = false
+ print("Autoattack deaktiviert")
+
+# Zauberstab aktivieren (deaktiviert Autoattack)
+func start_wand():
+ wand_active = true
+ autoattack_active = false
+ print("Zauberstab aktiviert")
+
+# Zauberstab deaktivieren
+func stop_wand():
+ wand_active = false
+ print("Zauberstab deaktiviert")
+
+# Zauberstab-Angriff ausführen (Fernkampf, magisch)
+func perform_wand_attack():
if target == null or not is_instance_valid(target):
target = null
+ wand_active = false
return
- if not can_attack:
+
+ var distance = global_position.distance_to(target.global_position)
+ if distance <= WAND_RANGE:
+ var dmg = get_attack_damage()
+ if target.has_method("take_damage_from"):
+ target.take_damage_from(dmg, level, false) # Magisch, ignoriert Rüstung
+ else:
+ target.take_damage(dmg)
+ print("Zauberstab: ", dmg, " magischer Schaden")
+ _play_attack_anim("autoattack")
+ trigger_global_cooldown()
+
+# Führt einen Autoattack aus (wird vom GCD-System aufgerufen)
+func perform_autoattack():
+ if target == null or not is_instance_valid(target):
+ target = null
+ autoattack_active = false
return
var distance = global_position.distance_to(target.global_position)
if distance <= get_attack_range():
- can_attack = false
var dmg = get_attack_damage()
- target.take_damage(dmg)
- print("Autoattack: ", dmg, " Schaden")
- await get_tree().create_timer(get_attack_cooldown()).timeout
- can_attack = true
- autoattack()
+ if target.has_method("take_damage_from"):
+ target.take_damage_from(dmg, level, true) # Nahkampf
+ else:
+ target.take_damage(dmg)
+ print("Autoattack: ", dmg, " Schaden (GCD: %.2fs, DPS: %.1f)" % [get_current_gcd(), get_dps()])
+ _play_attack_anim("autoattack")
+ trigger_global_cooldown()
+
+# Global Cooldown auslösen (basierend auf Waffe + Haste)
+func trigger_global_cooldown():
+ global_cooldown = get_current_gcd()
+
+# Heavy Strike: Starker Angriff mit Cooldown
+func use_heavy_strike():
+ if target == null or not is_instance_valid(target):
+ print("Kein Ziel für Heavy Strike!")
+ return
+
+ # Nur Skill-eigener Cooldown Check (kein GCD-Check!)
+ if heavy_strike_cooldown > 0:
+ print("Heavy Strike noch im Cooldown: ", "%.1f" % heavy_strike_cooldown, "s")
+ return
+
+ var distance = global_position.distance_to(target.global_position)
+ if distance > HEAVY_STRIKE_RANGE:
+ print("Ziel zu weit entfernt für Heavy Strike!")
+ return
+
+ var base_damage = randi_range(HEAVY_STRIKE_DAMAGE_MIN, HEAVY_STRIKE_DAMAGE_MAX)
+ var stat_bonus = int(get_main_stat() * CharacterClass.DAMAGE_PER_MAIN_STAT)
+ var damage = base_damage + stat_bonus
+ # Neues Schadenssystem mit Rüstung und Level-Differenz
+ if target.has_method("take_damage_from"):
+ target.take_damage_from(damage, level, true) # true = Nahkampf
else:
- await get_tree().create_timer(0.5).timeout
- autoattack()
+ target.take_damage(damage)
+ heavy_strike_cooldown = HEAVY_STRIKE_COOLDOWN
+ _play_attack_anim("heavy_strike")
+ trigger_global_cooldown() # GCD zurücksetzen damit Autoattack nicht sofort feuert
+ start_autoattack() # Autoattack nach Skill automatisch aktivieren
+ print("Heavy Strike! ", damage, " Rohschaden")
+
+# Frostblitz: Cast starten
+func use_frostbolt():
+ if is_casting:
+ return # Bereits am Casten
+ if target == null or not is_instance_valid(target):
+ print("Kein Ziel für Frostblitz!")
+ return
+ if frostbolt_cooldown > 0:
+ print("Frostblitz noch im Cooldown: ", "%.1f" % frostbolt_cooldown, "s")
+ return
+ if current_resource < FROSTBOLT_MANA_COST:
+ print("Nicht genug Mana für Frostblitz! (", current_resource, "/", FROSTBOLT_MANA_COST, ")")
+ return
+ var distance = global_position.distance_to(target.global_position)
+ if distance > FROSTBOLT_RANGE:
+ print("Ziel zu weit entfernt für Frostblitz!")
+ return
+
+ # Cast starten
+ _start_cast("frostbolt", FROSTBOLT_CAST_TIME)
+ print("Frostblitz wird gewirkt... (", FROSTBOLT_CAST_TIME, "s)")
+
+# Frostblitz: Schaden anwenden nach erfolgreichem Cast
+func _finish_frostbolt():
+ if target == null or not is_instance_valid(target):
+ print("Ziel verloren!")
+ return
+ var distance = global_position.distance_to(target.global_position)
+ if distance > FROSTBOLT_RANGE:
+ print("Ziel zu weit entfernt!")
+ return
+
+ # Mana abziehen
+ spend_resource(FROSTBOLT_MANA_COST)
+
+ var base_damage = randi_range(FROSTBOLT_DAMAGE_MIN, FROSTBOLT_DAMAGE_MAX)
+ var stat_bonus = int(intelligence * CharacterClass.DAMAGE_PER_MAIN_STAT)
+ var damage = base_damage + stat_bonus
+ if target.has_method("take_damage_from"):
+ target.take_damage_from(damage, level, false)
+ else:
+ target.take_damage(damage)
+ frostbolt_cooldown = FROSTBOLT_COOLDOWN
+ trigger_global_cooldown()
+ start_wand() # Zauberstab nach Cast weiter aktiv
+ print("Frostblitz! ", damage, " magischer Schaden (", FROSTBOLT_MANA_COST, " Mana)")
+
+# Cast-System
+func _start_cast(spell_id: String, cast_time: float):
+ is_casting = true
+ cast_spell_id = spell_id
+ cast_time_total = cast_time
+ cast_time_remaining = cast_time
+ autoattack_active = false # Autoattack pausieren während Cast
+ hud.show_castbar(spell_id, cast_time)
+
+func _cancel_cast():
+ if not is_casting:
+ return
+ is_casting = false
+ cast_spell_id = ""
+ cast_time_remaining = 0.0
+ hud.hide_castbar()
+ print("Zauber unterbrochen!")
+
+func _finish_cast():
+ var spell = cast_spell_id
+ is_casting = false
+ cast_spell_id = ""
+ cast_time_remaining = 0.0
+ hud.hide_castbar()
+ # Fertigen Zauber ausführen
+ match spell:
+ "frostbolt":
+ _finish_frostbolt()
# Raycast von der Kamera auf Mausposition — trifft Gegner mit take_damage()
func _try_select_target(start_attack: bool = false):
@@ -96,8 +934,43 @@ 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)
+ elif not start_attack:
+ # Nur bei Linksklick ins Leere: Ziel deselektieren und Autoattack stoppen
+ # Rechtsklick wird für Kameradrehung verwendet
+ clear_target()
func _physics_process(delta):
+ # Cast-System
+ if is_casting:
+ cast_time_remaining -= delta
+ hud.update_castbar(cast_time_total - cast_time_remaining, cast_time_total)
+ if cast_time_remaining <= 0:
+ _finish_cast()
+
+ # Global Cooldown herunterzählen (gilt für alle Aktionen)
+ if global_cooldown > 0:
+ global_cooldown -= delta
+
+ # Wenn GCD bereit und nicht am Casten
+ if global_cooldown <= 0 and not is_casting:
+ if wand_active:
+ perform_wand_attack()
+ elif autoattack_active:
+ perform_autoattack()
+
+ # Skill-Cooldowns herunterzählen
+ if heavy_strike_cooldown > 0:
+ heavy_strike_cooldown -= delta
+ if frostbolt_cooldown > 0:
+ frostbolt_cooldown -= delta
+ if potion_cooldown > 0:
+ potion_cooldown -= delta
+
+ # HUD Cooldowns aktualisieren - generisch pro Slot
+ for i in range(9):
+ var cd = _get_slot_cooldown(i)
+ hud.set_slot_cooldown(i, cd)
+
# Schwerkraft
if not is_on_floor():
velocity.y -= GRAVITY * delta
@@ -105,6 +978,8 @@ func _physics_process(delta):
# Springen
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
+ if is_casting:
+ _cancel_cast()
# Linksklick: nur markieren
if Input.is_action_just_pressed("select_target"):
@@ -114,32 +989,30 @@ func _physics_process(delta):
if Input.is_action_just_pressed("ui_right_mouse"):
_try_select_target(true)
- # Aktionsleiste 1-9
- if Input.is_action_just_pressed("action_1"):
- hud.set_active_slot(0)
- if target != null:
- autoattack()
- if Input.is_action_just_pressed("action_2"):
- hud.set_active_slot(1)
- if Input.is_action_just_pressed("action_3"):
- hud.set_active_slot(2)
- if Input.is_action_just_pressed("action_4"):
- hud.set_active_slot(3)
- if Input.is_action_just_pressed("action_5"):
- hud.set_active_slot(4)
- if Input.is_action_just_pressed("action_6"):
- hud.set_active_slot(5)
- if Input.is_action_just_pressed("action_7"):
- hud.set_active_slot(6)
- if Input.is_action_just_pressed("action_8"):
- hud.set_active_slot(7)
- if Input.is_action_just_pressed("action_9"):
- hud.set_active_slot(8)
+ # Aktionsleiste 1-9 — alle generisch über _use_action_slot
+ for i in range(9):
+ var action_name = "action_" + str(i + 1)
+ if Input.is_action_just_pressed(action_name):
+ hud.set_active_slot(i)
+ _use_action_slot(i)
# TEST: T drücken = 10 Schaden
if Input.is_action_just_pressed("test_damage"):
take_damage(10)
+ # C drücken = Charakter-Panel öffnen/schließen
+ if Input.is_action_just_pressed("toggle_character"):
+ character_panel.update_stats(self)
+ character_panel.toggle()
+
+ # I drücken = Inventar öffnen/schließen
+ if Input.is_action_just_pressed("toggle_inventory"):
+ inventory_panel.toggle()
+
+ # P drücken = Fähigkeiten-Panel öffnen/schließen
+ if Input.is_action_just_pressed("toggle_skills"):
+ skill_panel.toggle()
+
# Eingabe
var input_dir = Vector2.ZERO
if Input.is_action_pressed("move_forward"):
@@ -151,6 +1024,10 @@ func _physics_process(delta):
if Input.is_action_pressed("move_right"):
input_dir.x += 1
+ # Bewegung unterbricht Cast
+ if is_casting and input_dir.length() > 0:
+ _cancel_cast()
+
# Bewegung relativ zur Kamera
var world_yaw = rotation.y + camera_pivot.rotation.y
var forward = Vector3(-sin(world_yaw), 0, -cos(world_yaw)).normalized()
@@ -160,6 +1037,9 @@ func _physics_process(delta):
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
+ # Animation aktualisieren
+ _update_animation(input_dir)
+
# RMB gehalten: Spieler schaut in Kamerarichtung
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
rotation.y = world_yaw
diff --git a/player.tscn b/player.tscn
index 14906d5..202851e 100644
--- a/player.tscn
+++ b/player.tscn
@@ -1,9 +1,13 @@
[gd_scene format=3 uid="uid://dniyuebl8yhtv"]
[ext_resource type="Script" uid="uid://b6n25e5fh82ra" path="res://player.gd" id="1_4flbx"]
-[ext_resource type="PackedScene" uid="uid://01rrtitc6yh1" path="res://assets/kenney_blocky-characters_20/Models/GLB format/character-b.glb" id="2_hqtel"]
+[ext_resource type="PackedScene" uid="uid://da1w523lg7i2b" path="res://assets/models/warrior.fbx" id="2_hqtel"]
[ext_resource type="Script" uid="uid://bwtwon54po4w3" path="res://camera_pivot.gd" id="2_onrkg"]
[ext_resource type="PackedScene" uid="uid://bej3excyoxrdh" path="res://hud.tscn" id="4_hqtel"]
+[ext_resource type="PackedScene" uid="uid://character_panel" path="res://character_panel.tscn" id="5_char_panel"]
+[ext_resource type="PackedScene" uid="uid://inventory_panel" path="res://inventory_panel.tscn" id="6_inv_panel"]
+[ext_resource type="PackedScene" uid="uid://loot_window" path="res://loot_window.tscn" id="7_loot_win"]
+[ext_resource type="PackedScene" uid="uid://skill_panel" path="res://skill_panel.tscn" id="8_skill_panel"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4flbx"]
radius = 0.6
@@ -13,7 +17,7 @@ height = 3.0
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0)
script = ExtResource("1_4flbx")
-[node name="character-b2" parent="." unique_id=926968795 instance=ExtResource("2_hqtel")]
+[node name="PlayerModel" parent="." unique_id=926968795 instance=ExtResource("2_hqtel")]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1359412306]
@@ -27,3 +31,11 @@ script = ExtResource("2_onrkg")
transform = Transform3D(2, 0, 0, 0, 1.8126155, 0.84523654, 0, -0.84523654, 1.8126155, 0, 5, 5)
[node name="HUD" parent="." unique_id=1901284390 instance=ExtResource("4_hqtel")]
+
+[node name="CharacterPanel" parent="." instance=ExtResource("5_char_panel")]
+
+[node name="InventoryPanel" parent="." instance=ExtResource("6_inv_panel")]
+
+[node name="LootWindow" parent="." instance=ExtResource("7_loot_win")]
+
+[node name="SkillPanel" parent="." instance=ExtResource("8_skill_panel")]
diff --git a/player_backup.gd b/player_backup.gd
new file mode 100644
index 0000000..226fe32
--- /dev/null
+++ b/player_backup.gd
@@ -0,0 +1,251 @@
+# Player.gd
+# Steuert den Spielercharakter: Bewegung, Kamera, HP, Angriff, Zielauswahl
+extends CharacterBody3D
+
+const SPEED = 5.0
+const JUMP_VELOCITY = 4.5
+const GRAVITY = 9.8
+
+var max_hp = 100
+var current_hp = 100
+var target = null # Aktuell markierter Gegner
+var equipped_weapon = null # Ausgerüstete Waffe (null = unbewaffnet, Schaden = 1)
+
+# Global Cooldown System (GCD)
+var global_cooldown = 0.0
+const GLOBAL_COOLDOWN_TIME = 1.5 # GCD in Sekunden
+var autoattack_active = false # Ob Autoattack nach GCD weiterlaufen soll
+
+# Skills System - individuelle Cooldowns (zusätzlich zum GCD)
+var heavy_strike_cooldown = 0.0
+const HEAVY_STRIKE_DAMAGE_MIN = 10
+const HEAVY_STRIKE_DAMAGE_MAX = 15
+const HEAVY_STRIKE_COOLDOWN = 3.0
+const HEAVY_STRIKE_RANGE = 2.0
+
+@onready var camera_pivot = $CameraPivot
+@onready var camera = $CameraPivot/Camera3D
+@onready var hud = $HUD
+
+func _ready():
+ hud.update_health(current_hp, max_hp)
+ hud.set_active_slot(0)
+ # Icons für Skills setzen
+ hud.set_slot_icon(0, "res://icons/autoattack_icon.svg") # Slot 1: Autoattack
+ hud.set_slot_icon(1, "res://icons/heavy_strike_icon.svg") # Slot 2: Heavy Strike
+
+ # HUD-Klicks verbinden
+ hud.slot_clicked.connect(_on_slot_clicked)
+
+# Handler für HUD-Slot-Klicks
+func _on_slot_clicked(slot_index: int):
+ match slot_index:
+ 0: # Autoattack manuell starten
+ if target != null and global_cooldown <= 0:
+ start_autoattack()
+ perform_autoattack()
+ 1: # Heavy Strike
+ use_heavy_strike()
+
+# Schaden am Spieler abziehen und HP-Leiste aktualisieren
+func take_damage(amount):
+ current_hp = clamp(current_hp - amount, 0, max_hp)
+ hud.update_health(current_hp, max_hp)
+ if current_hp <= 0:
+ die()
+
+# HP heilen und HP-Leiste aktualisieren
+func heal(amount):
+ current_hp = clamp(current_hp + amount, 0, max_hp)
+ hud.update_health(current_hp, max_hp)
+
+func die():
+ print("Spieler gestorben!")
+
+# Schaden basierend auf ausgerüsteter Waffe (unbewaffnet = 1)
+func get_attack_damage() -> int:
+ if equipped_weapon == null:
+ return 1
+ return randi_range(equipped_weapon.min_damage, equipped_weapon.max_damage)
+
+# Reichweite basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5)
+func get_attack_range() -> float:
+ if equipped_weapon == null:
+ return 1.5
+ return equipped_weapon.range
+
+# Angriffsgeschwindigkeit basierend auf ausgerüsteter Waffe (unbewaffnet = 1.5s)
+func get_attack_cooldown() -> float:
+ if equipped_weapon == null:
+ return 1.5
+ return equipped_weapon.attack_speed
+
+# Ziel markieren — start_attack=true startet sofort die Autoattack
+func set_target(new_target, start_attack: bool = false):
+ if target != null and is_instance_valid(target):
+ target.hide_health()
+ target = new_target
+ target.show_health()
+ print("Ziel markiert: ", target.name)
+ if start_attack:
+ start_autoattack()
+
+# Autoattack aktivieren
+func start_autoattack():
+ autoattack_active = true
+ print("Autoattack aktiviert")
+
+# Autoattack deaktivieren
+func stop_autoattack():
+ autoattack_active = false
+ print("Autoattack deaktiviert")
+
+# Führt einen Autoattack aus (wird vom GCD-System aufgerufen)
+func perform_autoattack():
+ if target == null or not is_instance_valid(target):
+ target = null
+ autoattack_active = false
+ return
+
+ var distance = global_position.distance_to(target.global_position)
+ if distance <= get_attack_range():
+ var dmg = get_attack_damage()
+ target.take_damage(dmg)
+ print("Autoattack: ", dmg, " Schaden")
+ trigger_global_cooldown()
+ else:
+ print("Ziel zu weit entfernt für Autoattack")
+
+# Global Cooldown auslösen
+func trigger_global_cooldown():
+ global_cooldown = GLOBAL_COOLDOWN_TIME
+
+# Heavy Strike: Starker Angriff mit Cooldown
+func use_heavy_strike():
+ if target == null or not is_instance_valid(target):
+ print("Kein Ziel für Heavy Strike!")
+ return
+
+ # GCD Check
+ if global_cooldown > 0:
+ print("Global Cooldown aktiv: ", "%.1f" % global_cooldown, "s")
+ return
+
+ # Skill-eigener Cooldown Check
+ if heavy_strike_cooldown > 0:
+ print("Heavy Strike noch im Cooldown: ", "%.1f" % heavy_strike_cooldown, "s")
+ return
+
+ var distance = global_position.distance_to(target.global_position)
+ if distance > HEAVY_STRIKE_RANGE:
+ print("Ziel zu weit entfernt für Heavy Strike!")
+ return
+
+ var damage = randi_range(HEAVY_STRIKE_DAMAGE_MIN, HEAVY_STRIKE_DAMAGE_MAX)
+ target.take_damage(damage)
+ heavy_strike_cooldown = HEAVY_STRIKE_COOLDOWN
+ trigger_global_cooldown()
+ start_autoattack() # Autoattack nach Skill automatisch aktivieren
+ print("Heavy Strike! ", damage, " Schaden")
+
+# Raycast von der Kamera auf Mausposition — trifft Gegner mit take_damage()
+func _try_select_target(start_attack: bool = false):
+ var space_state = get_world_3d().direct_space_state
+ var viewport = get_viewport()
+ var mouse_pos = viewport.get_mouse_position()
+ var ray_origin = camera.project_ray_origin(mouse_pos)
+ var ray_end = ray_origin + camera.project_ray_normal(mouse_pos) * 100.0
+ var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
+ query.exclude = [self]
+ var result = space_state.intersect_ray(query)
+ if result and result.collider.has_method("take_damage"):
+ set_target(result.collider, start_attack)
+
+func _physics_process(delta):
+ # Global Cooldown herunterzählen
+ var gcd_was_active = global_cooldown > 0
+ if global_cooldown > 0:
+ global_cooldown -= delta
+
+ # Wenn GCD gerade abgelaufen ist und Autoattack aktiv, führe Autoattack aus
+ if gcd_was_active and global_cooldown <= 0 and autoattack_active:
+ perform_autoattack()
+
+ # Skill-Cooldowns herunterzählen
+ if heavy_strike_cooldown > 0:
+ heavy_strike_cooldown -= delta
+
+ # HUD Cooldowns aktualisieren
+ hud.set_slot_cooldown(0, global_cooldown) # Slot 1: GCD für Autoattack
+ hud.set_slot_cooldown(1, max(global_cooldown, heavy_strike_cooldown)) # Slot 2: max(GCD, Skill-CD)
+
+ # Schwerkraft
+ if not is_on_floor():
+ velocity.y -= GRAVITY * delta
+
+ # Springen
+ if Input.is_action_just_pressed("ui_accept") and is_on_floor():
+ velocity.y = JUMP_VELOCITY
+
+ # Linksklick: nur markieren
+ if Input.is_action_just_pressed("select_target"):
+ _try_select_target(false)
+
+ # Rechtsklick: markieren + angreifen
+ if Input.is_action_just_pressed("ui_right_mouse"):
+ _try_select_target(true)
+
+ # Aktionsleiste 1-9
+ if Input.is_action_just_pressed("action_1"):
+ hud.set_active_slot(0)
+ if target != null and global_cooldown <= 0:
+ start_autoattack()
+ perform_autoattack()
+ if Input.is_action_just_pressed("action_2"):
+ hud.set_active_slot(1)
+ use_heavy_strike()
+ if Input.is_action_just_pressed("action_3"):
+ hud.set_active_slot(2)
+ if Input.is_action_just_pressed("action_4"):
+ hud.set_active_slot(3)
+ if Input.is_action_just_pressed("action_5"):
+ hud.set_active_slot(4)
+ if Input.is_action_just_pressed("action_6"):
+ hud.set_active_slot(5)
+ if Input.is_action_just_pressed("action_7"):
+ hud.set_active_slot(6)
+ if Input.is_action_just_pressed("action_8"):
+ hud.set_active_slot(7)
+ if Input.is_action_just_pressed("action_9"):
+ hud.set_active_slot(8)
+
+ # TEST: T drücken = 10 Schaden
+ if Input.is_action_just_pressed("test_damage"):
+ take_damage(10)
+
+ # Eingabe
+ var input_dir = Vector2.ZERO
+ if Input.is_action_pressed("move_forward"):
+ input_dir.y -= 1
+ if Input.is_action_pressed("move_back"):
+ input_dir.y += 1
+ if Input.is_action_pressed("move_left"):
+ input_dir.x -= 1
+ if Input.is_action_pressed("move_right"):
+ input_dir.x += 1
+
+ # Bewegung relativ zur Kamera
+ var world_yaw = rotation.y + camera_pivot.rotation.y
+ var forward = Vector3(-sin(world_yaw), 0, -cos(world_yaw)).normalized()
+ var right = Vector3(cos(world_yaw), 0, -sin(world_yaw)).normalized()
+ var direction = (forward * -input_dir.y + right * input_dir.x)
+
+ velocity.x = direction.x * SPEED
+ velocity.z = direction.z * SPEED
+
+ # RMB gehalten: Spieler schaut in Kamerarichtung
+ if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
+ rotation.y = world_yaw
+ camera_pivot.rotation.y = 0
+
+ move_and_slide()
diff --git a/player_backup.gd.uid b/player_backup.gd.uid
new file mode 100644
index 0000000..60348d3
--- /dev/null
+++ b/player_backup.gd.uid
@@ -0,0 +1 @@
+uid://31iu6aebwmoc
diff --git a/project.godot b/project.godot
index b0f0c19..07b335d 100644
--- a/project.godot
+++ b/project.godot
@@ -97,6 +97,21 @@ ui_right_mouse={
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(411, 19),"global_position":Vector2(420, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
+toggle_character={
+"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":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
+]
+}
+toggle_inventory={
+"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":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null)
+]
+}
+toggle_skills={
+"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":80,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null)
+]
+}
[physics]
diff --git a/skill_panel.gd b/skill_panel.gd
new file mode 100644
index 0000000..6112763
--- /dev/null
+++ b/skill_panel.gd
@@ -0,0 +1,120 @@
+# SkillPanel.gd
+# Zeigt alle verfügbaren Fähigkeiten an, von hier aus auf Aktionsleiste ziehen
+extends CanvasLayer
+
+var panel_visible = false
+var player = null
+
+# Drag State
+var dragging = false
+var drag_skill_id: String = ""
+var drag_icon: TextureRect = null
+
+@onready var panel = $Panel
+@onready var skill_list = $Panel/VBoxContainer/ScrollContainer/SkillList
+
+func _ready():
+ panel.visible = false
+
+func setup(p):
+ player = p
+
+func toggle():
+ panel_visible = !panel_visible
+ panel.visible = panel_visible
+ if panel_visible:
+ _refresh_skills()
+
+func _refresh_skills():
+ if player == null:
+ return
+
+ for child in skill_list.get_children():
+ child.queue_free()
+
+ for skill in player.available_skills:
+ var hbox = HBoxContainer.new()
+ hbox.add_theme_constant_override("separation", 8)
+
+ # Icon
+ var icon_rect = TextureRect.new()
+ var tex = load(skill["icon"])
+ if tex:
+ icon_rect.texture = tex
+ icon_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon_rect.custom_minimum_size = Vector2(36, 36)
+ icon_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ hbox.add_child(icon_rect)
+
+ # Name + Beschreibung
+ var label = Label.new()
+ label.text = skill["name"]
+ label.add_theme_font_size_override("font_size", 14)
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ hbox.add_child(label)
+
+ # Tooltip
+ hbox.tooltip_text = skill["name"] + "\n" + skill["description"]
+ hbox.custom_minimum_size = Vector2(0, 40)
+ hbox.mouse_filter = Control.MOUSE_FILTER_STOP
+
+ # Drag starten bei Linksklick
+ var skill_id = skill["id"]
+ var skill_icon_path = skill["icon"]
+ hbox.gui_input.connect(_on_skill_input.bind(skill_id, skill_icon_path))
+
+ skill_list.add_child(hbox)
+
+func _on_skill_input(event: InputEvent, skill_id: String, icon_path: String):
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
+ _start_drag(skill_id, icon_path)
+
+func _start_drag(skill_id: String, icon_path: String):
+ dragging = true
+ drag_skill_id = skill_id
+ var tex = load(icon_path)
+ drag_icon = TextureRect.new()
+ drag_icon.texture = tex
+ drag_icon.custom_minimum_size = Vector2(40, 40)
+ drag_icon.size = Vector2(40, 40)
+ drag_icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ drag_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ drag_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ var drag_layer = CanvasLayer.new()
+ drag_layer.name = "DragLayer"
+ drag_layer.layer = 200
+ drag_layer.add_child(drag_icon)
+ get_tree().root.add_child(drag_layer)
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+ if player and player.hud:
+ player.hud.set_drag_active(true)
+
+func _process(_delta):
+ if dragging and drag_icon:
+ drag_icon.position = get_viewport().get_mouse_position() - Vector2(20, 20)
+ if player and player.hud:
+ player.hud.update_drag_hover(get_viewport().get_mouse_position())
+
+func _input(event):
+ if not dragging:
+ return
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
+ _end_drag()
+
+func _end_drag():
+ if not dragging:
+ return
+ if player and player.hud:
+ var slot_index = player.hud.get_slot_at_position(get_viewport().get_mouse_position())
+ if slot_index >= 0 and slot_index <= 8 and drag_skill_id != "":
+ player.assign_skill_to_action_bar(slot_index, drag_skill_id)
+ player.hud.set_drag_active(false)
+ if drag_icon:
+ var drag_layer = drag_icon.get_parent()
+ drag_layer.queue_free()
+ drag_icon = null
+ dragging = false
+ drag_skill_id = ""
diff --git a/skill_panel.gd.uid b/skill_panel.gd.uid
new file mode 100644
index 0000000..6ee7b18
--- /dev/null
+++ b/skill_panel.gd.uid
@@ -0,0 +1 @@
+uid://gf03d1ewoxcf
diff --git a/skill_panel.tscn b/skill_panel.tscn
new file mode 100644
index 0000000..bfe3d5a
--- /dev/null
+++ b/skill_panel.tscn
@@ -0,0 +1,48 @@
+[gd_scene load_steps=2 format=3 uid="uid://skill_panel"]
+
+[ext_resource type="Script" path="res://skill_panel.gd" id="1"]
+
+[node name="SkillPanel" type="CanvasLayer"]
+script = ExtResource("1")
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 0
+offset_left = 250
+offset_top = 100
+offset_right = 530
+offset_bottom = 400
+custom_minimum_size = Vector2(280, 300)
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 10
+offset_top = 10
+offset_right = -10
+offset_bottom = -10
+
+[node name="TitleLabel" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+text = "Fähigkeiten"
+horizontal_alignment = 1
+theme_override_font_sizes/font_size = 18
+
+[node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"]
+layout_mode = 2
+
+[node name="HintLabel" type="Label" parent="Panel/VBoxContainer"]
+layout_mode = 2
+text = "Linksklick + Ziehen auf Aktionsleiste"
+horizontal_alignment = 1
+theme_override_font_sizes/font_size = 10
+modulate = Color(0.7, 0.7, 0.7, 1)
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="SkillList" type="VBoxContainer" parent="Panel/VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
diff --git a/world.gd b/world.gd
index 3ee514d..bd28344 100644
--- a/world.gd
+++ b/world.gd
@@ -2,10 +2,88 @@
# Initialisiert die Spielwelt: weist dem Gegner den Spieler als Ziel zu
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
+
+# Startausrüstung
+const STARTER_SWORD = preload("res://equipment/iron_sword.tres")
+const STARTER_STAFF = preload("res://equipment/wooden_staff.tres")
+const STARTER_CHEST = preload("res://equipment/leather_chest.tres")
+
+# Loot Tables
+const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres")
+
+@onready var player = $Player
+
func _ready():
- var player = get_node("Player")
+ # Hauptmenü anzeigen
+ var main_menu = MAIN_MENU.instantiate()
+ add_child(main_menu)
+ main_menu.start_game.connect(_on_start_game)
+
+# 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
+func _on_class_selected(character_class: CharacterClass):
+ player.character_class = character_class
+
+ # Skills klassenabhängig aufbauen
+ player._init_class_skills()
+ for i in range(9):
+ player._refresh_action_slot(i)
+
+ # Startausrüstung klassenabhängig
+ if character_class.resource_type == CharacterClass.ResourceType.MANA:
+ player.equip_item(STARTER_STAFF)
+ else:
+ player.equip_item(STARTER_SWORD)
+ player.equip_item(STARTER_CHEST)
+
+ player._calculate_stats()
+ player.current_hp = player.max_hp
+ player.current_resource = player.max_resource
+ player.hud.update_health(player.current_hp, player.max_hp)
+ player.hud.update_resource(player.current_resource, player.max_resource, player.get_resource_name())
+ print("Klasse gewählt: ", character_class.class_name_de)
+
+ # Jetzt Gegner initialisieren
var enemy = get_node("Enemy")
+ _setup_enemy(enemy)
+
+# Gegner initialisieren und Signal verbinden
+func _setup_enemy(enemy):
if enemy and player:
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)
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):
+ print("Respawn in ", RESPAWN_TIME, " Sekunden...")
+ await get_tree().create_timer(RESPAWN_TIME).timeout
+ _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 6f1de4f..7b91309 100644
--- a/world.tscn
+++ b/world.tscn
@@ -3,7 +3,6 @@
[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="2_fj7yv"]
-[ext_resource type="Script" uid="uid://bg5qs3pcfp7p7" path="res://enemy.gd" id="4_aqk2v"]
[sub_resource type="BoxShape3D" id="BoxShape3D_fj7yv"]
size = Vector3(200, 0.5, 200)
@@ -16,12 +15,12 @@ size = Vector3(200, 0.5, 200)
[node name="World" type="Node3D" unique_id=2007838514]
script = ExtResource("1_tlwt5")
-[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=2101916269]
+[node name="Boden" type="StaticBody3D" parent="." unique_id=2101916269]
-[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=1873339390]
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Boden" unique_id=1873339390]
shape = SubResource("BoxShape3D_fj7yv")
-[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D" unique_id=1214783061]
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Boden" unique_id=1214783061]
mesh = SubResource("BoxMesh_tlwt5")
[node name="Player" parent="." unique_id=937297102 instance=ExtResource("1_f3sb7")]
@@ -32,7 +31,6 @@ transform = Transform3D(-45, 0, 0, 0, -45, 0, 0, 0, -45, 0, 0, 0)
[node name="Enemy" parent="." unique_id=332011146 instance=ExtResource("2_fj7yv")]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.3, -7.038)
-script = ExtResource("4_aqk2v")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=827244005]
navigation_mesh = SubResource("NavigationMesh_fj7yv")