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")