This commit is contained in:
Marek Lenczewski
2026-03-30 22:56:58 +02:00
parent d6715e9c3f
commit 04749104a0
49 changed files with 1406 additions and 171 deletions

View File

@@ -14,51 +14,75 @@ Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare n
## Architektur
- **Zwischen Szenen**: Kommunikation über EventBus (Autoload). Szenen kennen sich nicht.
- **Innerhalb einer Szene**: Modulare Skripte als Child-Nodes, Zugriff auf Geschwister-Nodes erlaubt.
- **Gruppen**: "player", "enemies", "portals"
- **Autoloads**: EventBus (Signals), GameState (Spielerzustand zwischen Szenenwechseln)
- **Gruppen**: "player", "enemies", "portals", "boss"
- **Resources** für statische Konfiguration (Stats, Abilities), **Nodes** für laufenden Zustand
- Portale sind keine Gegner — eigene Gruppe "portals", kein State/Aggro/Combat
## Projektstruktur
- `scenes/` — .tscn Dateien (world, player, enemy, portal, hud)
- `scripts/player/` — Spieler-Skripte (player, camera, movement, combat, targeting, role, respawn)
- `scripts/enemy/` — Gegner-Skripte (enemy, enemy_movement, enemy_combat, enemy_aggro)
- `scripts/portal/` — Portal-Skripte (portal)
- `scenes/` — .tscn Dateien
- `world.tscn` — Hauptszene (100x100m, Taverne in Mitte)
- `player/player.tscn` — Spieler
- `enemy/enemy.tscn` — Gegner
- `enemy/boss.tscn` — Boss (eigene Szene, erbt von Enemy)
- `portal/portal.tscn` — Portal (Gegner-Spawner)
- `portal/gate.tscn` — Gate (Teleporter, konfigurierbar: Dungeon-Eingang oder Exit)
- `dungeon/dungeon.tscn` — Dungeon (15x90m Schlauch, 4 Gegnergruppen + Boss)
- `hud/hud.tscn` — HUD
- `scripts/player/` — Spieler-Skripte (player, camera, movement, combat, targeting, role, respawn, hud)
- `scripts/enemy/` — Gegner-Skripte (enemy, enemy_movement, enemy_combat, enemy_aggro, boss)
- `scripts/portal/` — Portal + Gate (portal, gate)
- `scripts/dungeon/` — Dungeon-Logik (dungeon_manager)
- `scripts/world/` — Welt-Logik (portal_spawner)
- `scripts/components/` — Wiederverwendbare Komponenten (health, shield, healthbar, spawner)
- `scripts/abilities/` — Ability-System (ability, ability_set)
- `scripts/resources/` — Resource-Klassen (entity_stats)
- `resources/stats/` — Stats .tres (player_stats, enemy_stats, portal_stats)
- `resources/abilities/` — Ability .tres (single_attack, aoe_attack, utility_shield_reset, ult_burst, passive_damage_boost)
- `scripts/event_bus.gd` — Globale Signals
- `scripts/game_state.gd` — Spielerzustand zwischen Szenenwechseln
- `resources/stats/` — Stats .tres (player_stats, enemy_stats, portal_stats, boss_stats)
- `resources/abilities/` — Ability .tres pro Rolle (single_attack, tank_single, healer_single, etc.)
- `resources/ability_sets/` — AbilitySet .tres pro Rolle (tank_set, damage_set, healer_set)
## Planungsdokument
`mmo/plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand.
`plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand.
## Design-Dokumente
Unter `~/Documents/2026/projekte/mmo/` liegen die originalen Design-Docs:
Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design-Docs:
- `story.md` — Gameplay-Loop, Szenarien, Steuerung
- `idden.md` — Alle Ideen, Kernphilosophie, Technik
- `Szenarien.md` — Ressourcenanfragen, Dungeons, Gemeinschaft, Endgame
- `Level 1.md` bis `Level 3.md` — Systeme nach Priorität
## Core Loop
1. Portal spawnt auf der Karte
1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum)
2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%)
3. Spieler bekämpft Gegner mit Abilities (Single, AOE, Utility, Ult) + Auto-Attack
4. Portal bei 0 HP → Phase 2 (geplant: wird Gate zu Dungeon)
5. Tod → 3s Respawn am Startpunkt
4. Portal bei 0 HP → Gate spawnt, Gegner werden entfernt
5. Spieler betritt Gate → Dungeon (separate Szene, 4 Gegnergruppen + Boss)
6. Spieler kann zwischen Welt und Dungeon hin und her (beide Gates aktiv solange Boss lebt)
7. Boss stirbt → 2s Delay → Spieler wird zur Taverne teleportiert, Gates verschwinden
8. Tod → 3s Respawn bei Taverne
## Kampfsystem
- Auto-Attack: 10 Schaden, 1s, automatisch bei anvisiertem Gegner im Kampf
- 5 Abilities pro Rolle: Single, AOE, Utility, Ult, Passive
- Auto-Attack: Rollenspezifisch (D: 10 Schaden/10m, T: 5 Schaden/3m, H: 1 Heilung/20m), 0.5s CD
- 5 Abilities pro Rolle: Single, AOE, Utility, Ult, Passive — jede Rolle hat eigene Werte
- GCD 0.5s (gilt für 1, 2, 4), Utility ignoriert GCD
- Cooldown-Overlay in AbilityBar (von oben nach unten)
- Heilung: Heiler heilt sich selbst (Singleplayer), is_heal Flag auf Ability
- Passive: Schadens-Boost (D), Schild-Boost (T), Heal-Boost (H) — je 50%
- Targeting: Klick, TAB (cyclet "enemies" + "portals"), Auto-Target (Gegner > Portal)
- Aggro-System: 1:1 Schaden, Tank 2x, verfällt -1/s, exponentiell außerhalb Portal-Radius
- Aggro-System: 1:1 Schaden, Tank 2x, Heilung 0.5x, verfällt -1/s, exponentiell außerhalb Portal-Radius
## Rollen
- Tank (T), Schaden (D), Heiler (H) — wechselbar mit ALT+1/2/3
- Aktuell alle gleiche Abilities, später unterschiedlich
- Jede Rolle hat eigenes AbilitySet (Resource)
- Jede Rolle hat eigenes AbilitySet mit unterschiedlichen Werten und Mechaniken
- Tank: Weniger Schaden, Nahkampf, Schild-Ult (300%), Schild-Passive
- Heiler: Heilt statt schadet (Single, AOE, Ult), Heal-Passive, AOE macht Schaden
## Szenenwechsel
- GameState Autoload speichert Spielerzustand (HP, Shield, Rolle) zwischen Szenen
- Gate (Eingang): save_player → Dungeon laden
- Gate (Exit): save_player, returning_from_dungeon → Welt laden, Spieler bei Gate-Position
- Boss-Tod: dungeon_cleared → Welt laden, Spieler bei Taverne, Gates weg
- PortalSpawner stellt Gate wieder her wenn portal_position gesetzt und Boss noch lebt
## Workflow mit dem User
- **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren

46
infosammlung/Level 1.md Normal file
View File

@@ -0,0 +1,46 @@
Hauptsysteme
- Welt
- Spieler
- Gegner
- NPC
- Dungeons
- HUD
- Fähigkeiten
- Klassen
Zusatzsysteme
- Berufe
- Quest
- Items
- Inventar
- Equipment
Nebensysteme
- Audio
- Licht
- Transport
- Tageszeit
- Wetter
- Presitige
- Einstellungen
- Achivments
- Events
- Tutorial
- Speichersystem
- Game Over
- Admin-Tools
- Narrative
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Chat
- Gruppe
- Handel
- Offline-Fortschritt
---
Nicht
- Gilden - Jeder ist in Teil der Community
- Housing - Fokus auf Tarverne und Zentrum
- Kosmetik - Aussehen durch Crafting und Fortschritt bestimmt

60
infosammlung/Level 2.md Normal file
View File

@@ -0,0 +1,60 @@
Hauptsysteme
- Welt
- Spieler
- Gegner
- NPC
- Dungeons
- HUD
- KI
- Kampf
- Fähigkeiten
- Eigenschaften
- Klassen
- Berufe
- Quest
- Items
- Inventar
- Equipment
- Loot
- Fortschritt
Nebensysteme
- Audio
- Licht
- Transport
- Tageszeit
- Wetter
- Presitige
- Einstellungen
- Achivments
- Events
- Tutorial
- Speichersystem
- Game Over
- Admin-Tools
- Narrative
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Chat
- Gruppe
- Handel
- Offline-Fortschritt
---
Nicht
- Gilden - Jeder ist in Teil der Community
- Housing - Fokus auf Tarverne und Zentrum
- Kosmetik - Aussehen durch Crafting und Fortschritt bestimmt
Alex / Sanni
Billi / Kai
Lea / Christopf
Dösi
Nina / Igor
Basti
Vincent
Bennet

110
infosammlung/Level 3.md Normal file
View File

@@ -0,0 +1,110 @@
Hauptsysteme
- Welt
- Boden, Hügel, Gebäude, Natur
- Spieler
- Mesh, Textur, Steuerung, Animation
- Verwandlung - Kampf und Normale Gestalt
- Gestaltanpassung - Primärattribut > Rest
- Elemente verändern Aussehen
- Gegner
- Mesh, Textur, Animation
- Skalierung - Passend sich dem Level an
- NPC
- Mesh, Textur, Animation
- Fallback - Ein NPC je mögliche Spieler Position
- KI
- Gegner KI
- NPC KI
- Kampf
- Schadensberechnung
- Angriff
- Bei Tod respawn in der Stadt
- AFK - Unbesiegbar, Castet automatisch Fähigkeiten mit halber Stärke, wenn in Gruppe
- Dungeons
- Gebietgenerierung
- Instanzierung - Eine Instanz, tauchen zufällig auf
- Break - Dungon öffnet nach einer Weile, Elite Monster starten eine Invasion
- Invasion - Kein Respawn
- Loot - 50% Gruppenleistung und 50% Eigenleistung
- HUD
- Leben, Schild, Name, Stufe
- Fähigkeiten
- Minimap
- Anpassung
- Fähigkeiten
- Arten - Single, AOE, Utility, Ulti, Passive
- Elemente manipulieren Fähigkeiten
- Eigenschaften
- Leben, Schild
- Attribute
- Automatische verbesserung mit Aktionen
- Klassen
- Klassenauswahl
- Arten - Verteidigung, Nahkampf, Fernkampf, Heilung, Unterstützung
- Wechsel - Rollenwechsel geht immer
- Berufe
- Übersicht, Annehmen, Wechseln
- Arten - Abendteurer, Quester, Schmied, Baumeister, Forscher, Archologe
- Abendteurer - Monster bekämpfen
- Forschung - Unbekannte Objekte identifizieren
- Crafting - Objekte erstellen oder verbessern, Ressourcen abbauen
- Bauen - Objekte erstellen oder verbessern, Ressourcen abbauen
- Entdecken - Unbekannte Objekte finden
- Quest
- Öffnen, Annehmen, Abbrechen, Schließen
- Erstellen - Quester kann eigene Quests erstellen
- Schwarzes Brett - Aktuelle Aufgaben von NPCs oder Spielern
- Items
- Mesh, Textur
- Inventar
- Items aufnehmen und ablegen
- Größe unendlich
- Equipment
- Loot
- Fortschritt
- Aktionen geben Erfahrung
- Globaler Fortschritt - Jede Aktion gibt einen Teil an jeden Spieler
- Globale Freischaltung - Jede Freischaltung gilt für alle Spieler
Nebensysteme
- Audio
- Musik, Sounds
- Licht
- Lokal, Global
- Transport
- Extremschnell auf Wegen
- Tageszeit
- Tag, Abend, Nacht
- Wetter
- Sonne, Regen, Schnee
- Presitige
- Einstellungen
- Achivments
- Spielfortschritt, Entdecken, Maxlevel
- Events
- Tutorial
- Keins, Gameplay selbsterklärend gestalten
- Speichersystem
- Speichern, Laden
- Game Over
- Tarverne wird zerstört
- Belohnung - Teil vom Fortschritt gibt Erharung zum Prestige
- Admin-Tools
- Narrative
- Geschichte durch Welt / NPC / Berichte erzählen
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Client, Server, Lokalität
- Chat
- Lokaler Chat
- Globaler Chat
- Gruppen Chat
- Gruppe
- Spieler in der nähe bilden automatisch eine Gruppe
- Gruppensuche
- Handel
- Einfaches Handelfenster
- Offline-Fortschritt

26
infosammlung/Planung.md Normal file
View File

@@ -0,0 +1,26 @@
Baum
- Welt
- Spieler
Welt - Node3D
- Boden - MeshInstance3D
- PlaneMesh 10mx10m
- StandardMaterial3D mit NoiseTexture2D als Albedo
- NoiseTexture2D: FastNoiseLite, Gradient dunkelgrün zu hellgrün, seamless
- UV-Skalierung für Wiederholung
- DirectionalLight3D — Sonnenlicht mit Schatten, 45° Winkel
- Camera3D — 30° Winkel nach unten geneigt
Spieler - CharacterBody3D
- Kollision - CollisionShape3D mit CapsuleShape3D
- Mesh - MeshInstance3D mit CapsuleMesh
- Kamera
- CameraPivot - Node3D, Position am Kopf des Spielers
- Camera3D - Position hinter/über dem Spieler
- Stript - LMB Kamera und Laufrichtung bewegen, RMB Kamera bewegen
- Steuerung
- Skript - WASD Bewegung relativ zur Kamera, Leertaste Springen
Gegner - CharacterBody3D
- Kollision - CollisionShape3D
- Mesh - MeshInstance3D mit CapsuleMesh, Rot

38
infosammlung/Szenarien.md Normal file
View File

@@ -0,0 +1,38 @@
# Szenarien
Ressourcenanfragen
- Heiler will Zauber verbessern -> Ressourcen fehlen -> Anfrage erstellen
- Quester sieht die Anfrage > Quester nimmt sie an und erstellt eine Quest
- Abenteurer sieht eine Quest -> Nimmt Quest an und besorgt die Ressource
- Abendteuert gibt Ressource an Quester -> Bekommt Erfahrung, Gold und Ruf
- Quester gibt Ressource -> Bekommt Erfahrung, Gold und Ruf
- Heiler bekommt Ressource -> Zahl Geld -> Verbessert die Fähigkieten
Dungeons
- Portal taucht auf -> Wird eingestufft und auf der Karte angezeigt
- Abedteurer können das Portal betreten und es leeren -> Erfahrung und Ressourcen
- Miner können Ressourcen abbauen -> Verkaufen für Erfahrung und Gold
- Archologen können Portal untersuchen -> Forschung verbessern
- Dungen wird nicht besiegt -> Dungeon bricht und greift das Dorf an
- Wird das Hauptgebäude zerstört -> Kein Respawn Möglich -> Dorf stirbt
Gemeinschaft
- Bewohner erledigen aufgaben -> Ort entwickelt sich und wird stärker
- Je Stärker der Ort, desto schwer sind die Aufgaben
- Stärkere Spieler fokussieren sich auf schwere Aufgabe
- Anfänger auf einfache Aufgaben
- Gemeinschaft bekommt Vorteile durch jeden Spieler
- Wird ein Spieler besser dann wird die Geminschaft besser
Endgame
- Gemeinschaft ist fertig ausgebaut
- Durchschnitt der Spieler hat ein bestimmtes Niveau erreicht
- "Spiel wird einfach"
- Letzte Invasion -> Portale tauchen auf -> Greifen dorf an -> Spieler verteidigen sich
- Kein Respawn -> Am Ende alle Gegner oder Spiele sind besiegt
- Sieg für Spieler -> Prestige Fähigkeiten
- Sieg für Gegner -> Dorf stirbt -> Game Over
Loop
- Dorf beitretten -> Gemeinschaft entsteht -> Gemeinschaft wächst
- Gemeinschaft überlebt Invasion -> Prestige und Reset
- Neues Dorf --> ...

70
infosammlung/idden.md Normal file
View File

@@ -0,0 +1,70 @@
## Alle Ideen Übersicht
**Kernphilosophie**
- Spiel gegen die drei Urängste: soziale Zurückweisung, Überforderung, Versagen
- Angst nicht entfernen, sondern durch positive Gefühle ersetzen
- Zielgruppe: einsame Menschen, denen eine Brücke zur echten Welt gebaut wird
- Stilles Designprinzip, nicht offen als "Anti-Einsamkeits-Spiel" kommuniziert
**Lokalität**
- Spieler werden nach geographischer Nähe im gleichen Layer gruppiert
- Dynamischer Radius: bei geringer Dichte wird er vergrößert bis global
- Globaler Server als Opt-out für Spieler, die Anonymität wollen
- Ziel: Online-Freundschaften können natürlich zu echten werden
- Treffen ist selbstorganisiert, kein eingebautes Feature
**Spieldesign**
- Jede Aktion hilft automatisch anderen, auch Solospielen
- Keine formellen Gruppen jeder in einem Bereich ist automatisch Teil des Geschehens
- Dungeons spawnen als Invasionen auf der Karte mit verschiedenen Schwierigkeiten
- Boss skaliert dynamisch mit Spieleranzahl
- Versagen eines Einzelnen schadet nie der Gruppe
- Kein Blame, kein DPS-Meter, kein Kick-System
**Dynamische Gemeinschaft**
- Spieler können Abenteurer oder "NPCs" sein Schmied, Questgeber, Händler, Gastwirt
- Stadt-Spieler und Abenteurer sind voneinander abhängig
- Offline-Automatismen: Läden und Quests laufen weiter wenn der Spieler offline ist
- Die Welt fühlt sich immer belebt an
**Progressionssystem**
- Berufungs-Zähler wie in Plunderer jede Tätigkeit hat einen sichtbaren Rang
- Individuelle Ränge stärken die gesamte Gemeinschaft
- Regionale Gesamtstärke schaltet bessere Inhalte frei
- Kein Neid, da jeder Aufstieg allen hilft
- Natürliche Spezialisierung durch offene Nischen
**Technik**
- Godot 4 als Client
- Dedizierter Server mit API-Architektur
- Client sendet Intentionen, Server validiert
- Offline-Automatismen laufen serverseitig
- Anti-Cheat by Design: selbst Cheater helfen der Gemeinschaft
**Monetarisierung**
- Free-to-Play mit kosmetischen Items
- Cosmetics werden wertvoller durch Lokalität und soziale Bindung
Hier die neuen Punkte aus unserer Diskussion:
- Jede Spielerrolle hat ein NPC-Pendant als Fallback das Spiel funktioniert solo, wird aber mit Spielern besser
- Invasionen skalieren mit Anzahl und Stärke der Spieler
- Solo-Invasion ist die einfachste Stufe, stärkste Invasionen nur auf öffentlichen Servern
- Soziale Angst abbauen, aber Verlustangst als Motivator nutzen
- Gemeinsame Vorbereitung auf die Endinvasion ist das Endgame-Ziel
- Prestige-Verbesserungen erweitern den Spielstil statt nur Werte zu erhöhen
- Prestige schaltet einzigartige Fähigkeiten frei, darunter Wiederbelebung
- Normales Spiel hat normalen Respawn
- Endinvasion: kein normaler Respawn, nur Prestige-Spieler können wiederbeleben
- Prestige-Spieler werden im Finale zum Sicherheitsnetz der Gemeinschaft
- Kernloop: Bedeutung aufbauen → alles riskieren → Triumph → Prestige-Reset → neu beginnen mit erweiterten Fähigkeiten
- Anfangs Godot 2D für den Prototyp für Core Loops testing
- Später Godot 3D mit Blender Objekten mit wenigen Polygonen
- Kleine Karte Anfangs

58
infosammlung/story.md Normal file
View File

@@ -0,0 +1,58 @@
Shortstory
- Dorf in der Mitte
- Portale spawnen außen mit Monstern und einem Boss
- Nach 1 Tag bricht das Portal und Monter greifen Dorf an
- Spieler besigen Monster und bekommen Essenz
- Teil der gewonnen Essenz wird an alle Spieler verteilt
- Spieler können Kampfrollen annehmen Tank, Schaden, Heiler
- Spieler haben 5 Fähigkeiten: Single, AOE, Utility, Ult, Passive
- Fähigkeiten unterscheiden sich je nach Kampfrolle
- Spieler können Elemente verwenden, sie verändern die Fähigkeiten
- Spieler können Berufe annehmen: Quester, Crafter, Forscher, Archologe, Handwerker
- Berufe verbessern den Kampf und Verteidigung
- Nach 7 Tagen kommt eine Invasion mit starken Gegnern
- Beim Sieg bekommt man Presitigeessenz, die man für dauerhafte Verbesserungen verwenden kann
- Wird das Dorf zerstört, dann heißt es Game Over
Allgemein
- Eine Welt startet mit einem Dorfplatz und einem Helfer.
- Der Helfer soll neuen Spielern helfen.
- Die Welt besteht aus einem Bereich.
- In dem Berech erscheinen Portale, diese sind auf der Karte zu sehen.
- In dem Portal sind Monster und ein Boss.
- Nach einem Tag bricht das Portal und die Monster greifen das Dorf an.
- Stirbt der Helfer, dann heißt es Game Over.
- Stirbt ein Spieler, dann wird er am Dorfplatz wiederbelebt.
- Spieler können Monster für Loot und Erfahrung bekämpfen.
- Jeder Spieler in der Welt bekommt einen kleinen Teil vom Loot.
- Mit Loot kann man Inhalte freischalten.
- Inhalte sind sowasw wie Beruffreischaltung, Fähigkeitenverbesserungen, Elementverbesserungen, Items
- Berufe sind Abenteurer - Monster bekämpfen, Archologe - Neues finden, Forscher - Neues identifizieren, Handwerker - Gebäude erstellen und verbessern, Crafter - Items erstellen und verbessern, Quester - Aufgaben verwalten
- Elemente wie Feuer, Eis, Finternis, Gift, ... verändern wie Fähigkeiten funktionieren
- Wenn man Monster bekämpft bekommt man automatisch Attribute je nach der Role - Tank, Schaden, Heiler
- Der Beruf hat die Stufen F,E,D,C,B,A,S,S+, je höher die Stufe, desto mehr kann man machen
- Die Attribute, Elemente und der höchste Berufsrang verändern das Aussehen
- Nach 7 Tagen kommt eine Invasion mit starken Gegnern, dabei gibt es kein Respawn, wird der Helfer getötet dann heißt es Game Over, man bekommt % der geschafften Invasion als Prestige gutgeschrieben
- Besiegt man die Invasion, dann endet das Spiel mit 100% und einem sicheren Prestigepunkt
Szenarien
- Spiel start
- Menü - Einzelspiel, Koop, Online, Einstellungen, Quit
- Einzelspiel - Neues Spiel, Laden
- Neues Spiel - Name eingeben, dann startet das Spiel
- Neues Spieler erscheit
- HUD links-oben - Leben, Schild, Rang
- HUD mitte-unten - Kampfhaltung, Fähigkeiten
- HUD rechts-oben - Minimap
- Fähigkeiten - Single, AOE, Utility, Ult, Passive (1 bis 5)
- Kapfhaltung - Tank, Schaden, Heiler, Neutral (ALT+1 bis ALT+4)
- Laufen mit WASD
- Leertaste springen
- Linke Maustaste Kamera bewegen und Laufrichtung mitziehn
- Rechte Maustaste nur Kamera bewegen
- Welt - Boden, Hügel, Natur
- Weg - Bewegung +300%
- Dorf - Nur Tarverne

220
plan.md
View File

@@ -1,8 +1,9 @@
# Projektstruktur
## Szenenbaum
- Welt
- Taverne
- Player
- Portal
- Portale (dynamisch)
- Gegner
- HUD
@@ -10,23 +11,26 @@
- Zwischen Szenen: Kommunikation über EventBus (Szenen kennen sich nicht)
- Innerhalb einer Szene: Modulare Skripte, Zugriff auf Geschwister-Nodes erlaubt
## Szenen
- world.tscn — Hauptszene
## Welt
- world.tscn — Hauptszene (200x200m)
- NavigationRegion3D (Wegfindung für Gegner)
- Boden (MeshInstance3D, 20x20m PlaneMesh, Gras-Noise-Textur)
- Boden (MeshInstance3D, 200x200m PlaneMesh, Gras-Noise-Textur)
- Kollision (StaticBody3D, WorldBoundaryShape3D)
- Licht (DirectionalLight3D, 45°, Schatten)
- Spieler (Instanz von player.tscn)
- Portal (Instanz von portal.tscn)
- Taverne (StaticBody3D, BoxMesh, Mitte der Karte, Spieler-Spawn/Respawn)
- Spieler (Instanz von player.tscn, Spawn bei Taverne)
- PortalSpawner (Node, portal_spawner.gd)
- HUD (Instanz von hud.tscn)
- portal_spawner.gd — Spawnt Portale dynamisch in der Portalzone (außerhalb Dorfradius)
- Timer-basiert, zufällige Position, max. Anzahl begrenzt
- player.tscn — Spieler
## Spieler
- player.tscn — CharacterBody3D
- Gruppe (player)
- CharacterBody3D (player.gd)
- Kollision (CapsuleShape3D, 1.8m x 0.3m)
- Mesh (CapsuleMesh)
- CameraPivot (Node3D, Kopfhöhe, camera.gd)
- Camera3D (hinter/über Spieler)
- Camera3D (hinter/über Spieler)w
- Movement (Node, movement.gd)
- Combat (Node, combat.gd)
- Role (Node, role.gd)
@@ -34,36 +38,51 @@
- Health (Node, health.gd)
- Shield (Node, shield.gd)
- Respawn (Node, respawn.gd)
- player.gd — Kern, verbindet Komponenten
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung anpassen
- movement.gd — Bewegung (WASD relativ zur Kamera), Springen, Schwerkraft
- respawn.gd — Bei Tod: Spieler deaktivieren, 3s Timer, Respawn bei Taverne, Leben/Schild voll
- player_stats.tres — health=100, regen=1/s, shield=50, delay=3s, regen_time=5s
- hud.tscn — HUD (eigene Szene, kommuniziert nur über Events)
- CanvasLayer (hud.gd)
- HealthBar (ProgressBar, links oben)
- ShieldBar (ProgressBar, links oben, unter HealthBar)
- RespawnTimer (Label, mitte, Countdown bei Tod)
- AbilityBar (HBoxContainer, mitte unten)
- RoleIcon (Label, T=Tank, D=Schaden, H=Heiler)
- Abilities(Label, Fähigkeiten 1-4, Passive P)
## Kampf
- combat.gd — Führt Abilities aus, verwaltet Cooldowns (GCD 0.5s), Auto-Attack (0.5s)
- targeting.gd — Klick/TAB anvisieren (Gruppen "enemies" + "portals"), Kampfmodus bei Gegner-Angriff, Auto-Targeting (Gegner > Portal)
- role.gd — Rollenwechsel ALT+1 bis ALT+3 (Tank/Schaden/Heiler), tauscht AbilitySet
- portal.tscn — Portal (Gegner-Spawner, anvisierbar)
- Gruppe (portals)
- StaticBody3D (portal.gd)
- Kollision (CylinderShape3D)
- Mesh (CylinderMesh, blau)
- Health (Node, health.gd)
- HitArea (Area3D, Trefferbereich)
- CollisionShape3D
- Spawner (Node, spawner.gd)
- DetectionArea (Area3D, 10m Radius, Auto-Targeting bei Betreten)
- CollisionShape3D (SphereShape3D)
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
- Phase 1: Angreifbar, HP-Bar, spawnt 3 Gegner bei 85%/70%/55%/40%/25%/10% Leben
- Portal = Spawnpunkt, 10m Aggro-Radius
- Phase 2 (geplant): Bei 0 HP → Portal wird Gate, teleportiert in Dungeon
- Abgesperrter Bereich mit Gegner-Gruppen und Boss
## Abilities
- ability.gd (Resource) — name, type, damage, range, cooldown, uses_gcd, execute()
- ability_set.gd (Resource) — Set von 5 Abilities pro Rolle
- ability_modifier.gd (Resource) — Verändert Ability (Element, Beruf, Prestige)
- Typen: Single, AOE, Utility, Ult, Passive
- Auto-Attack: Automatisch bei anvisiertem Gegner im Kampf
- Schadens-Klasse:
- AA: 10 Schaden, 10m
- 1 Single: 30 Schaden, 20m Range, 2s CD, GCD
- 2 AOE: 20 Schaden, 5m Range, 3s CD, GCD
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
- 4 Ult: 5x Single (50 Schaden) + 2x AOE 3m (20 Schaden), 20m Range, 15s CD, GCD
- 5 Passive: 50% mehr Schaden Aura, 50m (permanent aktiv, kein CD)
- Tank-Klasse:
- AA: 5 Schaden, 3m
- 1 Single: 15 Schaden, 3m Range, 2s CD, GCD
- 2 AOE: 10 Schaden, 10m Range, 3s CD, GCD
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
- 4 Ult: Schild 300%, 20s CD, GCD
- 5 Passive: 50% mehr Schild Aura, 50m (permanent aktiv, kein CD)
- Heiler-Klasse: Kein Autotarget, Anvisierter Spieler wird geheilt, ist keiner anvisiert dann Selfheal
- AA: 1 Heilung, 20m
- 1 Single: 15 Heilung, 20m Range, 2s CD, GCD
- 2 AOE: 10 Heilung, 20m Range, 3s CD, GCD
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
- 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD
- 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD)
- Jede Rolle hat ein eigenes AbilitySet
- Beim Rollenwechsel wird das AbilitySet getauscht
- Elemente und Modifikatoren verändern Abilities nachträglich
- enemy.tscn — Gegner
## Gegner
- enemy.tscn — CharacterBody3D
- Gruppe (enemies)
- CharacterBody3D (enemy.gd)
- Kollision (CapsuleShape3D)
- Mesh (SphereMesh, Platzhalter)
- Health (Node, health.gd)
@@ -81,36 +100,12 @@
- Border (ColorRect, gelb, sichtbar bei Anvisierung)
- HealthBar (ProgressBar, grün)
- ShieldBar (ProgressBar, blau)
## Skripte
- player.gd — Kern, verbindet Komponenten
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung anpassen
- movement.gd — Bewegung (WASD relativ zur Kamera), Springen, Schwerkraft
- combat.gd — Führt Abilities aus, verwaltet Cooldowns (GCD 0.5s), Auto-Attack (10 Schaden, 1s, 20m)
- targeting.gd — Klick/TAB anvisieren (Gruppen "enemies" + "portals"), Kampfmodus bei Gegner-Angriff, Auto-Targeting (Gegner > Portal)
- event_bus.gd — Autoload-Singleton, globale Signals
- role.gd — Rollenwechsel ALT+1 bis ALT+3 (Tank/Schaden/Heiler), tauscht AbilitySet
- respawn.gd — Bei Tod: Spieler deaktivieren, 3s Timer, Respawn am Startpunkt, Leben/Schild voll
- hud.gd — Reagiert auf Events, aktualisiert HealthBar/ShieldBar/AbilityBar/RespawnTimer
- portal.gd — Portal-Kern, Gruppe "portals", stirbt bei 0 HP, kein Kampf/Aggro/State
- enemy.gd — Gegner-Kern, State Machine (Idle, Verfolgen, Angreifen, Zurückkehren), alarmiert Gegner in 3m, Gruppe "enemies"
- enemy_movement.gd — Navigation zum Ziel/Spawnpunkt
- enemy_combat.gd — Angriff über Event (damage_requested)
- enemy_aggro.gd — Aggro-Tabelle (Spieler → Wert), wählt Ziel mit höchstem Aggro
## Components (scripts/components/, wiederverwendbar)
- health.gd — Leben, Regeneration, Tod bei 0 (liest Base-Werte aus EntityStats)
- shield.gd — Schild, Regeneration nach Delay (liest Base-Werte aus EntityStats)
- healthbar.gd — Liest Health/Shield (optional) vom Parent, gelber Rand bei Anvisierung, blauer Lebensbalken wenn Spieler Ziel ist (nur bei Gegnern)
- spawner.gd — Spawnt Entities bei Lebensschwellen, engagt Spieler im Portal-Radius sofort
## Stats (Resources)
- entity_stats.gd (Resource) — max_health, health_regen, max_shield, shield_regen_delay, shield_regen_time
- player_stats.tres — health=100, regen=1/s, shield=50, delay=3s, regen_time=5s
- enemy_stats.tres — health=100, regen=0, shield=50, delay=3s, regen_time=5s
- portal_stats.tres — health=500, regen=0, shield=0
## Aggro
- Aggro-Regeln:
- Schaden = Aggro (1:1)
- Heilung = Aggro (0.5x)
- Tank = Aggro-Multiplikator (2x)
@@ -120,35 +115,104 @@
- Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert 10% Leben/s bis 100%, dann 1%/s
- Bei Spieler-Tod → Aggro auf 0
## Abilities (Resources)
- ability.gd (Resource) — name, type, damage, range, cooldown, uses_gcd, execute()
- ability_set.gd (Resource) — Set von 5 Abilities pro Klasse
- ability_modifier.gd (Resource) — Verändert Ability (Element, Beruf, Prestige)
- Typen: Single, AOE, Utility, Ult, Passive
- Auto-Attack: 10 Schaden, 1s, automatisch bei anvisiertem Gegner im Kampf
- Schadens-Klasse:
- 1 Single: 30 Schaden, 20m Range, 1s CD, GCD
- 2 AOE: 20 Schaden, 5m Range, 3s CD, GCD
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
- 4 Ult: 5x Single (50 Schaden) + 2x AOE 3m (20 Schaden), 20m Range, 15s CD, GCD
- 5 Passive: 50% mehr Schaden (permanent aktiv, kein CD)
- Jede Rolle hat ein eigenes AbilitySet
- Beim Rollenwechsel wird das AbilitySet getauscht
- Elemente und Modifikatoren verändern Abilities nachträglich
## Portal
- portal.tscn — StaticBody3D
- Gruppe (portals)
- Kollision (CylinderShape3D)
- Mesh (CylinderMesh, blau)
- Health (Node, health.gd)
- HitArea (Area3D, Trefferbereich)
- CollisionShape3D
- Spawner (Node, spawner.gd)
- DetectionArea (Area3D, 10m Radius, Auto-Targeting bei Betreten)
- CollisionShape3D (SphereShape3D)
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
- portal.gd — Portal-Kern, Gruppe "portals", bei 0 HP → spawnt Gate, zerstört Gegner und sich
- spawner.gd — Spawnt Entities bei Lebensschwellen, engagt Spieler im Portal-Radius sofort
- portal_stats.tres — health=500, regen=1%/s, shield=0
- Phase 1: Angreifbar, HP-Bar, spawnt 3 Gegner bei 85%/70%/55%/40%/25%/10% Leben
- Portal = Spawnpunkt, 10m Aggro-Radius
## Gate
- gate.tscn — StaticBody3D (keine Kollision)
- Mesh (CylinderMesh, grün, leuchtend)
- GateArea (Area3D, 3m Radius, Spieler-Erkennung)
- CollisionShape3D (SphereShape3D)
- gate.gd — Konfigurierbar: target_scene, is_exit
- Eingangs-Gate: Prüft nach 0.5s ob Spieler bereits in GateArea steht, speichert Portal-Position
- Exit-Gate: 1s Aktivierungsdelay (verhindert Re-Triggern bei Spawn)
- Beide Gates bestehen solange Boss lebt, verschwinden bei dungeon_cleared
## Dungeon
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
- NavigationRegion3D (Wegfindung)
- Boden (PlaneMesh, dunkle Textur)
- 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
- Spieler (Instanz von player.tscn, Position 0/1/-5)
- HUD (Instanz von hud.tscn)
- Gegnergruppen bei (15x15, 15x30, 15x45, 15x60), je 4 Gegner
- Boss (Instanz von boss.tscn)
- Exit-Gate (Instanz von gate.tscn, is_exit=true, Ecke bei -6/0/-4)
- DungeonManager (Node, dungeon_manager.gd)
- dungeon_manager.gd — Stellt Spieler wieder her, erkennt Boss-Tod, 2s Delay, dungeon_cleared, zurück zur Taverne
## Boss
- boss.tscn — CharacterBody3D (boss.gd)
- Gruppe (enemies, boss)
- Kollision (CapsuleShape3D, größer als Enemy)
- Mesh (SphereMesh, lila, 1.5x Größe)
- Health (Node, health.gd, boss_stats.tres)
- Shield (Node, shield.gd, boss_stats.tres)
- HitArea, DetectionArea, NavigationAgent3D, EnemyMovement, EnemyCombat, EnemyAggro
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
- boss.gd — Erbt von enemy.gd, Gruppe "boss"
- boss_stats.tres — health=500, regen=0, shield=100, delay=5s, regen_time=8s
## GameState
- game_state.gd — Autoload, speichert Spielerzustand zwischen Szenenwechseln
- save_player(player), restore_player(player), clear_player(), clear()
- returning_from_dungeon — Spieler kommt aus Dungeon, spawnt bei Gate-Position
- dungeon_cleared — Boss tot, Gates werden entfernt, Spieler spawnt bei Taverne
- portal_position (Vector3, Position des Gates für Rückkehr + Wiederherstellung)
## Gemeinsame Komponenten
- health.gd — Leben, Regeneration, Tod bei 0 (liest Base-Werte aus EntityStats)
- shield.gd — Schild, Regeneration nach Delay (liest Base-Werte aus EntityStats)
- healthbar.gd — Liest Health/Shield (optional) vom Parent, gelber Rand bei Anvisierung, blauer Lebensbalken wenn Spieler Ziel ist (nur bei Gegnern)
- entity_stats.gd (Resource) — max_health, health_regen, max_shield, shield_regen_delay, shield_regen_time
## HUD
- hud.tscn — CanvasLayer (hud.gd)
- HealthBar (ProgressBar, links oben, Label "50/100")
- ShieldBar (ProgressBar, links oben, unter HealthBar, Label "25/50")
- RespawnTimer (Label, mitte, Countdown bei Tod)
- AbilityBar (HBoxContainer, mitte unten)
- RoleIcon (Label, T=Tank, D=Schaden, H=Heiler)
- Abilities (Label, Fähigkeiten 1-4, Passive P)
- hud.gd — Reagiert auf Events, aktualisiert HealthBar/ShieldBar/AbilityBar/RespawnTimer
## Events
- event_bus.gd — Autoload-Singleton, globale Signals
- Kampf:
- attack_executed(attacker, position, direction, damage) — Angriff wurde ausgeführt
- damage_dealt(attacker, target, damage) — Schaden wurde verteilt
- damage_requested(attacker, target, amount) — Schaden zwischen Szenen anfordern
- Entity:
- entity_died(entity) — Entity ist gestorben
- health_changed(entity, current, max) — Leben hat sich verändert
- shield_changed(entity, current, max) — Schild hat sich verändert
- shield_broken(entity) — Schild ist auf 0 gefallen
- shield_regenerated(entity) — Schild ist wieder voll
- Spieler:
- target_changed(player, target) — Spieler hat neues Ziel anvisiert
- player_respawned(player) — Spieler ist respawnt
- role_changed(player, role_type) — Spieler hat Rolle gewechselt
- health_changed(entity, current, max) — Leben hat sich verändert
- shield_changed(entity, current, max) — Schild hat sich verändert
- respawn_tick(timer) — Respawn-Countdown Update
- enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert
- cooldown_tick(cooldowns, max_cooldowns, gcd_timer) — Cooldown-Update für HUD
- Gegner:
- enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert
- Portal:
- portal_spawn(portal, enemies) — Portal hat Gegner gespawnt
- portal_defeated(portal) — Portal besiegt, wird Gate
- Dungeon:
- dungeon_cleared() — Boss tot, Dungeon gesäubert

View File

@@ -18,6 +18,11 @@ config/icon="res://icon.svg"
[autoload]
EventBus="*res://scripts/event_bus.gd"
GameState="*res://scripts/game_state.gd"
[dotnet]
project/assembly_name="mmo"
[input]

View File

@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://m1kgk2uugnex"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Circle of Healing"
type = 1
damage = 10.0
ability_range = 20.0
cooldown = 3.0
icon = "2"
is_heal = true

View File

@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Heal Aura"
type = 4
damage = 50.0
ability_range = 0.0
uses_gcd = false
icon = "P"
passive_stat = "heal"

View File

@@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://cqw1jy6kqvmnj"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Heal"
damage = 15.0
ability_range = 20.0
cooldown = 2.0
icon = "1"
is_heal = true

View File

@@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://d04nu1leyki16"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Divine Light"
type = 3
damage = 25.0
ability_range = 20.0
cooldown = 15.0
aoe_radius = 3.0
icon = "4"
is_heal = true

View File

@@ -7,5 +7,5 @@ script = ExtResource("1")
ability_name = "Single"
damage = 30.0
ability_range = 20.0
cooldown = 1.0
cooldown = 2.0
icon = "1"

View File

@@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Tank AOE"
type = 1
damage = 10.0
ability_range = 10.0
cooldown = 3.0
icon = "2"

View File

@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Shield Aura"
type = 4
damage = 50.0
ability_range = 0.0
uses_gcd = false
icon = "P"
passive_stat = "shield"

View File

@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Tank Strike"
damage = 15.0
ability_range = 3.0
cooldown = 2.0
icon = "1"

View File

@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Fortress"
type = 2
damage = 300.0
cooldown = 20.0
icon = "4"

View File

@@ -2,12 +2,13 @@
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scripts/abilities/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1_ability"]
[ext_resource type="Resource" path="res://resources/abilities/single_attack.tres" id="2"]
[ext_resource type="Resource" path="res://resources/abilities/aoe_attack.tres" id="3"]
[ext_resource type="Resource" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" path="res://resources/abilities/ult_burst.tres" id="5"]
[ext_resource type="Resource" path="res://resources/abilities/passive_damage_boost.tres" id="6"]
[ext_resource type="Resource" uid="uid://dwvc8b3cmce8l" path="res://resources/abilities/single_attack.tres" id="2"]
[ext_resource type="Resource" uid="uid://bpx3l13iuynfv" path="res://resources/abilities/aoe_attack.tres" id="3"]
[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" uid="uid://s32wvlww2ls2" path="res://resources/abilities/ult_burst.tres" id="5"]
[ext_resource type="Resource" uid="uid://dadpl32yujwhe" path="res://resources/abilities/passive_damage_boost.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 25.0

View File

@@ -2,12 +2,15 @@
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scripts/abilities/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1_ability"]
[ext_resource type="Resource" path="res://resources/abilities/single_attack.tres" id="2"]
[ext_resource type="Resource" path="res://resources/abilities/aoe_attack.tres" id="3"]
[ext_resource type="Resource" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" path="res://resources/abilities/ult_burst.tres" id="5"]
[ext_resource type="Resource" path="res://resources/abilities/passive_damage_boost.tres" id="6"]
[ext_resource type="Resource" path="res://resources/abilities/healer_single.tres" id="2"]
[ext_resource type="Resource" path="res://resources/abilities/healer_aoe.tres" id="3"]
[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" path="res://resources/abilities/healer_ult.tres" id="5"]
[ext_resource type="Resource" path="res://resources/abilities/healer_passive.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 1.0
aa_range = 20.0
aa_is_heal = true

View File

@@ -2,12 +2,14 @@
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scripts/abilities/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1_ability"]
[ext_resource type="Resource" path="res://resources/abilities/single_attack.tres" id="2"]
[ext_resource type="Resource" path="res://resources/abilities/aoe_attack.tres" id="3"]
[ext_resource type="Resource" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" path="res://resources/abilities/ult_burst.tres" id="5"]
[ext_resource type="Resource" path="res://resources/abilities/passive_damage_boost.tres" id="6"]
[ext_resource type="Resource" path="res://resources/abilities/tank_single.tres" id="2"]
[ext_resource type="Resource" path="res://resources/abilities/tank_aoe.tres" id="3"]
[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/abilities/utility_shield_reset.tres" id="4"]
[ext_resource type="Resource" path="res://resources/abilities/tank_ult.tres" id="5"]
[ext_resource type="Resource" path="res://resources/abilities/tank_passive.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 5.0
aa_range = 3.0

View File

@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"]
[resource]
script = ExtResource("1")
max_health = 500.0
health_regen = 0.0
max_shield = 100.0
shield_regen_delay = 5.0
shield_regen_time = 8.0

157
scenes/dungeon/dungeon.tscn Normal file
View File

@@ -0,0 +1,157 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"]
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"]
[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"]
[ext_resource type="Script" path="res://scripts/dungeon/dungeon_manager.gd" id="dungeon_manager"]
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0)
polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
albedo_color = Color(0.2, 0.18, 0.15, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_1"]
material = SubResource("StandardMaterial3D_floor")
size = Vector2(15, 90)
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
albedo_color = Color(0.25, 0.22, 0.2, 1)
[sub_resource type="BoxMesh" id="BoxMesh_north_south"]
material = SubResource("StandardMaterial3D_wall")
size = Vector3(15, 3, 0.5)
[sub_resource type="BoxShape3D" id="BoxShape3D_north_south"]
size = Vector3(15, 3, 0.5)
[sub_resource type="BoxMesh" id="BoxMesh_east_west"]
material = SubResource("StandardMaterial3D_wall")
size = Vector3(0.5, 3, 90)
[sub_resource type="BoxShape3D" id="BoxShape3D_east_west"]
size = Vector3(0.5, 3, 90)
[node name="Dungeon" type="Node3D"]
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_1")
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 40)
mesh = SubResource("PlaneMesh_1")
[node name="BodenCollision" type="StaticBody3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
shape = SubResource("WorldBoundaryShape3D_1")
[node name="WallSouth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -5.25)
[node name="Mesh" type="MeshInstance3D" parent="WallSouth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallSouth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallNorth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 85.25)
[node name="Mesh" type="MeshInstance3D" parent="WallNorth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallNorth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallEast" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallEast"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallEast"]
shape = SubResource("BoxShape3D_east_west")
[node name="WallWest" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallWest"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallWest"]
shape = SubResource("BoxShape3D_east_west")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 40)
light_energy = 0.6
shadow_enabled = true
[node name="Player" parent="." instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -3)
[node name="HUD" parent="." instance=ExtResource("hud")]
[node name="Enemy1a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 15)
[node name="Enemy1b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 15)
[node name="Enemy1c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 15)
[node name="Enemy1d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 15)
[node name="Enemy2a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 30)
[node name="Enemy2b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 30)
[node name="Enemy2c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 30)
[node name="Enemy2d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 30)
[node name="Enemy3a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 45)
[node name="Enemy3b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 45)
[node name="Enemy3c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 45)
[node name="Enemy3d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 45)
[node name="Enemy4a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 60)
[node name="Enemy4b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 60)
[node name="Enemy4c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60)
[node name="Enemy4d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
[node name="Boss" parent="." instance=ExtResource("boss")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75)
[node name="ExitGate" parent="." instance=ExtResource("gate")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
target_scene = "res://scenes/world.tscn"
is_exit = true
[node name="DungeonManager" type="Node" parent="."]
script = ExtResource("dungeon_manager")

128
scenes/enemy/boss.tscn Normal file
View File

@@ -0,0 +1,128 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://scripts/enemy/boss.gd" id="1"]
[ext_resource type="Script" path="res://scripts/components/health.gd" id="2"]
[ext_resource type="Script" path="res://scripts/components/shield.gd" id="3"]
[ext_resource type="Script" path="res://scripts/components/healthbar.gd" id="4"]
[ext_resource type="Script" path="res://scripts/enemy/enemy_movement.gd" id="5"]
[ext_resource type="Script" path="res://scripts/enemy/enemy_combat.gd" id="6"]
[ext_resource type="Script" path="res://scripts/enemy/enemy_aggro.gd" id="7"]
[ext_resource type="Resource" path="res://resources/stats/boss_stats.tres" id="8"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.6
height = 2.0
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
radius = 0.6
height = 2.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
bg_color = Color(0.3, 0.1, 0.1, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
bg_color = Color(0.2, 0.8, 0.2, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill_aggro"]
bg_color = Color(0.2, 0.4, 0.9, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.6, 0.1, 0.6, 1)
[sub_resource type="SphereMesh" id="SphereMesh_1"]
radius = 0.75
height = 1.5
material = SubResource("StandardMaterial3D_1")
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 8.0
[node name="Boss" type="CharacterBody3D"]
script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0)
mesh = SubResource("SphereMesh_1")
[node name="Health" type="Node" parent="."]
script = ExtResource("2")
stats = ExtResource("8")
[node name="Shield" type="Node" parent="."]
script = ExtResource("3")
stats = ExtResource("8")
[node name="HitArea" type="Area3D" parent="."]
collision_layer = 4
collision_mask = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
shape = SubResource("CapsuleShape3D_2")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
[node name="EnemyMovement" type="Node" parent="."]
script = ExtResource("5")
[node name="EnemyCombat" type="Node" parent="."]
script = ExtResource("6")
[node name="EnemyAggro" type="Node" parent="."]
script = ExtResource("7")
[node name="DetectionArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 1
monitoring = true
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"]
shape = SubResource("SphereShape3D_1")
[node name="Healthbar" type="Sprite3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0)
billboard = 1
pixel_size = 0.01
script = ExtResource("4")
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
transparent_bg = true
size = Vector2i(104, 29)
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"]
offset_right = 104.0
offset_bottom = 29.0
color = Color(1, 0.9, 0.2, 1)
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"]
offset_left = 2.0
offset_top = 2.0
offset_right = 102.0
offset_bottom = 12.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
max_value = 500.0
value = 500.0
show_percentage = false
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport"]
offset_left = 2.0
offset_top = 15.0
offset_right = 102.0
offset_bottom = 27.0
theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill")
max_value = 100.0
value = 100.0
show_percentage = false
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"]

View File

@@ -48,6 +48,18 @@ max_value = 100.0
value = 100.0
show_percentage = false
[node name="HealthLabel" type="Label" parent="HealthBar"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 12
text = "100/100"
horizontal_alignment = 1
vertical_alignment = 1
[node name="ShieldBar" type="ProgressBar" parent="."]
offset_left = 10.0
offset_top = 35.0
@@ -59,6 +71,18 @@ max_value = 50.0
value = 50.0
show_percentage = false
[node name="ShieldLabel" type="Label" parent="ShieldBar"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 12
text = "50/50"
horizontal_alignment = 1
vertical_alignment = 1
[node name="RespawnTimer" type="Label" parent="."]
anchors_preset = 8
anchor_left = 0.5

36
scenes/portal/gate.tscn Normal file
View File

@@ -0,0 +1,36 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/portal/gate.gd" id="1"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.1, 0.9, 0.3, 1)
emission_enabled = true
emission = Color(0.1, 0.9, 0.3, 1)
emission_energy_multiplier = 0.5
[sub_resource type="CylinderMesh" id="CylinderMesh_1"]
top_radius = 1.0
bottom_radius = 1.0
height = 2.0
material = SubResource("StandardMaterial3D_1")
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 3.0
[node name="Gate" type="StaticBody3D"]
script = ExtResource("1")
collision_layer = 0
collision_mask = 0
[node name="Mesh" type="MeshInstance3D" parent="."]
mesh = SubResource("CylinderMesh_1")
[node name="GateArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 1
monitoring = true
[node name="CollisionShape3D" type="CollisionShape3D" parent="GateArea"]
shape = SubResource("SphereShape3D_1")
[connection signal="body_entered" from="GateArea" to="." method="_on_gate_area_body_entered"]

View File

@@ -2,10 +2,10 @@
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
[ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"]
[ext_resource type="PackedScene" path="res://scenes/portal/portal.tscn" id="portal"]
[ext_resource type="Script" uid="uid://cskx6o07iukwh" path="res://scripts/world/portal_spawner.gd" id="portal_spawner"]
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
vertices = PackedVector3Array(-9.5, 0.5, -9.5, -9.5, 0.5, 9.5, 9.5, 0.5, 9.5, 9.5, 0.5, -9.5)
vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5)
polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)]
[sub_resource type="Gradient" id="Gradient_1"]
@@ -19,37 +19,56 @@ noise = SubResource("FastNoiseLite_1")
color_ramp = SubResource("Gradient_1")
seamless = true
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"]
albedo_texture = SubResource("NoiseTexture2D_1")
uv1_scale = Vector3(3, 3, 1)
uv1_scale = Vector3(15, 15, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_1"]
material = SubResource("StandardMaterial3D_1")
size = Vector2(20, 20)
material = SubResource("StandardMaterial3D_ground")
size = Vector2(100, 100)
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1"]
[node name="World" type="Node3D" unique_id=1834775183]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tavern"]
albedo_color = Color(0.45, 0.3, 0.15, 1)
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=561649622]
[sub_resource type="BoxMesh" id="BoxMesh_tavern"]
material = SubResource("StandardMaterial3D_tavern")
size = Vector3(5, 3, 5)
[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"]
size = Vector3(5, 3, 5)
[node name="World" type="Node3D" unique_id=1865233338]
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679]
navigation_mesh = SubResource("NavigationMesh_1")
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=1129907783]
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=593226019]
mesh = SubResource("PlaneMesh_1")
[node name="BodenCollision" type="StaticBody3D" parent="." unique_id=391155617]
[node name="BodenCollision" type="StaticBody3D" parent="." unique_id=1112667638]
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision" unique_id=809444866]
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision" unique_id=621554623]
shape = SubResource("WorldBoundaryShape3D_1")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1132129079]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1797472817]
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
shadow_enabled = true
[node name="Player" parent="." unique_id=190642073 instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, 1, -7)
[node name="Taverne" type="StaticBody3D" parent="." unique_id=1978646562]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="HUD" parent="." unique_id=24362518 instance=ExtResource("hud")]
[node name="Mesh" type="MeshInstance3D" parent="Taverne" unique_id=2043279810]
mesh = SubResource("BoxMesh_tavern")
[node name="Portal" parent="." unique_id=2086501116 instance=ExtResource("portal")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne" unique_id=2108564286]
shape = SubResource("BoxShape3D_tavern")
[node name="Player" parent="." unique_id=585018813 instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5)
[node name="HUD" parent="." unique_id=763693646 instance=ExtResource("hud")]
[node name="PortalSpawner" type="Node" parent="." unique_id=2100621428]
script = ExtResource("portal_spawner")

View File

@@ -11,9 +11,12 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var uses_gcd: bool = true
@export var aoe_radius: float = 0.0
@export var icon: String = ""
@export var is_heal: bool = false
@export var passive_stat: String = "damage"
func execute(player: Node, targeting: Node) -> bool:
var dmg: float = _get_modified_damage(player, damage)
var stat: String = "heal" if is_heal else "damage"
var dmg: float = _get_modified_damage(player, damage, stat)
match type:
Type.SINGLE:
return _execute_single(player, targeting, dmg)
@@ -25,28 +28,44 @@ func execute(player: Node, targeting: Node) -> bool:
return _execute_ult(player, targeting, dmg)
return false
func _get_modified_damage(player: Node, base: float) -> float:
func _get_modified_damage(player: Node, base: float, stat: String = "damage") -> float:
var combat: Node = player.get_node("Combat")
return combat.apply_passive(base)
return combat.apply_passive(base, stat)
func _in_range(player: Node, targeting: Node) -> bool:
if ability_range <= 0:
return true
var target: Node3D = targeting.current_target
if not target or not is_instance_valid(target):
if is_heal:
return true
if not is_instance_valid(targeting.current_target):
return false
var dist: float = player.global_position.distance_to(target.global_position)
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
return dist <= ability_range
func _execute_single(player: Node, targeting: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
if not _in_range(player, targeting):
return false
var target: Node3D = targeting.current_target
EventBus.damage_requested.emit(player, target, dmg)
if not is_instance_valid(targeting.current_target):
return false
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
func _execute_aoe(player: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
var players := player.get_tree().get_nodes_in_group("player")
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= ability_range:
EventBus.heal_requested.emit(player, p, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
var hit := false
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
@@ -61,20 +80,38 @@ func _execute_aoe(player: Node, dmg: float) -> bool:
func _execute_utility(player: Node) -> bool:
var shield: Node = player.get_node_or_null("Shield")
if shield:
if damage > 0:
shield.current_shield = shield.max_shield * (damage / 100.0)
else:
if shield.current_shield >= shield.max_shield:
return false
shield.current_shield = shield.max_shield
EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield)
return true
return false
func _execute_ult(player: Node, targeting: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
var players := player.get_tree().get_nodes_in_group("player")
var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= aoe_range:
EventBus.heal_requested.emit(player, p, dmg * 0.4)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
if not _in_range(player, targeting):
return false
if not is_instance_valid(targeting.current_target):
return false
var target: Node3D = targeting.current_target
EventBus.damage_requested.emit(player, target, dmg * 5.0)
var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if enemy != target:
if enemy != target and is_instance_valid(enemy):
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
if enemy_dist <= aoe_range:
EventBus.damage_requested.emit(player, enemy, dmg * 2.0)

View File

@@ -2,3 +2,6 @@ extends Resource
class_name AbilitySet
@export var abilities: Array[Ability] = []
@export var aa_damage: float = 10.0
@export var aa_range: float = 10.0
@export var aa_is_heal: bool = false

View File

@@ -10,6 +10,7 @@ func _ready() -> void:
health_regen = stats.health_regen
current_health = max_health
EventBus.damage_requested.connect(_on_damage_requested)
EventBus.heal_requested.connect(_on_heal_requested)
func _process(delta: float) -> void:
if current_health > 0 and current_health < max_health and health_regen > 0:
@@ -34,3 +35,11 @@ func take_damage(amount: float, attacker: Node) -> void:
EventBus.health_changed.emit(get_parent(), current_health, max_health)
if current_health <= 0:
EventBus.entity_died.emit(get_parent())
func heal(amount: float) -> void:
current_health = min(current_health + amount, max_health)
EventBus.health_changed.emit(get_parent(), current_health, max_health)
func _on_heal_requested(healer: Node, target: Node, amount: float) -> void:
if target == get_parent():
heal(amount)

View File

@@ -7,11 +7,15 @@ var regen_time: float
var current_shield: float
var regen_timer := 0.0
var base_max_shield: float
func _ready() -> void:
max_shield = stats.max_shield
base_max_shield = max_shield
regen_delay = stats.shield_regen_delay
regen_time = stats.shield_regen_time
current_shield = max_shield
EventBus.role_changed.connect(_on_role_changed)
func _process(delta: float) -> void:
if max_shield <= 0:
@@ -25,6 +29,22 @@ func _process(delta: float) -> void:
EventBus.shield_regenerated.emit(get_parent())
EventBus.shield_changed.emit(get_parent(), current_shield, max_shield)
func _on_role_changed(_player: Node, _role_type: int) -> void:
if get_parent() != _player:
return
var role: Node = get_parent().get_node_or_null("Role")
if not role:
return
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
return
max_shield = base_max_shield
for ability in ability_set.abilities:
if ability and ability.type == Ability.Type.PASSIVE and ability.passive_stat == "shield":
max_shield = base_max_shield * (1.0 + ability.damage / 100.0)
current_shield = min(current_shield, max_shield)
EventBus.shield_changed.emit(get_parent(), current_shield, max_shield)
func absorb(amount: float) -> float:
if current_shield <= 0:
return amount

View File

@@ -0,0 +1,16 @@
extends Node
func _ready() -> void:
var player: Node = get_tree().get_first_node_in_group("player")
if player:
GameState.restore_player(player)
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if entity.is_in_group("boss"):
await get_tree().create_timer(2.0).timeout
GameState.dungeon_cleared = true
GameState.returning_from_dungeon = false
GameState.clear_player()
EventBus.dungeon_cleared.emit()
get_tree().change_scene_to_file("res://scenes/world.tscn")

View File

@@ -0,0 +1 @@
uid://drn4h1lxx5t1j

5
scripts/enemy/boss.gd Normal file
View File

@@ -0,0 +1,5 @@
extends "res://scripts/enemy/enemy.gd"
func _ready() -> void:
super._ready()
add_to_group("boss")

View File

@@ -0,0 +1 @@
uid://bkehq3dqyp2yd

View File

@@ -10,6 +10,7 @@ var seconds_outside := 0.0
func _ready() -> void:
EventBus.damage_dealt.connect(_on_damage_dealt)
EventBus.entity_died.connect(_on_entity_died)
EventBus.heal_requested.connect(_on_heal_requested)
func _process(delta: float) -> void:
var outside_portal := false
@@ -53,6 +54,12 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
multiplier = 2.0
add_aggro(attacker, amount * multiplier)
func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void:
if not healer.is_in_group("player"):
return
if healer in aggro_table:
add_aggro(healer, amount * 0.5)
func _on_entity_died(entity: Node) -> void:
aggro_table.erase(entity)

View File

@@ -15,3 +15,6 @@ signal respawn_tick(timer)
signal enemy_engaged(enemy, target)
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
signal portal_spawn(portal, enemies)
signal heal_requested(healer, target, amount)
signal portal_defeated(portal)
signal dungeon_cleared()

42
scripts/game_state.gd Normal file
View File

@@ -0,0 +1,42 @@
extends Node
var player_health: float = -1.0
var player_max_health: float = -1.0
var player_shield: float = -1.0
var player_max_shield: float = -1.0
var player_role: int = 1
var portal_position: Vector3 = Vector3.ZERO
var returning_from_dungeon := false
var dungeon_cleared := false
func save_player(player: Node) -> void:
var health: Node = player.get_node("Health")
var shield: Node = player.get_node("Shield")
var role: Node = player.get_node("Role")
player_health = health.current_health
player_max_health = health.max_health
player_shield = shield.current_shield
player_max_shield = shield.max_shield
player_role = role.current_role
func restore_player(player: Node) -> void:
if player_health < 0:
return
var health: Node = player.get_node("Health")
var shield: Node = player.get_node("Shield")
var role: Node = player.get_node("Role")
health.current_health = player_health
shield.current_shield = player_shield
role.set_role(player_role)
EventBus.health_changed.emit(player, health.current_health, health.max_health)
EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield)
func clear_player() -> void:
player_health = -1.0
player_shield = -1.0
func clear() -> void:
clear_player()
portal_position = Vector3.ZERO
returning_from_dungeon = false
dungeon_cleared = false

View File

@@ -0,0 +1 @@
uid://cp2vadwcd12sm

View File

@@ -1,9 +1,7 @@
extends Node
const GCD_TIME := 0.5
const AA_DAMAGE := 10.0
const AA_COOLDOWN := 1.0
const AA_RANGE := 20.0
const AA_COOLDOWN := 0.5
@onready var player: CharacterBody3D = get_parent()
@onready var targeting: Node = get_parent().get_node("Targeting")
@@ -36,12 +34,23 @@ func _auto_attack(delta: float) -> void:
return
if not is_instance_valid(targeting.current_target):
return
var dist := player.global_position.distance_to(targeting.current_target.global_position)
if dist > AA_RANGE:
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
return
var dmg := apply_passive(AA_DAMAGE)
var aa_damage: float = ability_set.aa_damage
var aa_range: float = ability_set.aa_range
var aa_is_heal: bool = ability_set.aa_is_heal
var dmg: float = apply_passive(aa_damage, "heal" if aa_is_heal else "damage")
if aa_is_heal:
EventBus.heal_requested.emit(player, player, dmg)
print("AA Heal: %s an %s" % [dmg, player.name])
else:
var dist := player.global_position.distance_to(targeting.current_target.global_position)
if dist > aa_range:
return
var target_name: String = targeting.current_target.name
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
print("AA: %s Schaden an %s" % [dmg, targeting.current_target.name])
print("AA: %s Schaden an %s" % [dmg, target_name])
aa_timer = AA_COOLDOWN
func _load_abilities() -> void:
@@ -74,11 +83,11 @@ func _unhandled_input(event: InputEvent) -> void:
gcd_timer = GCD_TIME
return
func apply_passive(base_damage: float) -> float:
func apply_passive(base: float, stat: String = "damage") -> float:
for ability in abilities:
if ability and ability.type == Ability.Type.PASSIVE:
return base_damage * (1.0 + ability.damage / 100.0)
return base_damage
if ability and ability.type == Ability.Type.PASSIVE and ability.passive_stat == stat:
return base * (1.0 + ability.damage / 100.0)
return base
func _on_role_changed(_player: Node, _role_type: int) -> void:
_load_abilities()

View File

@@ -3,7 +3,9 @@ extends CanvasLayer
const GCD_TIME := 0.5
@onready var health_bar: ProgressBar = $HealthBar
@onready var health_label: Label = $HealthBar/HealthLabel
@onready var shield_bar: ProgressBar = $ShieldBar
@onready var shield_label: Label = $ShieldBar/ShieldLabel
@onready var respawn_label: Label = $RespawnTimer
@onready var class_icon: Label = $AbilityBar/ClassIcon/Label
@onready var ability_panels: Array = [
@@ -30,11 +32,13 @@ func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
health_bar.max_value = max_val
health_bar.value = current
health_label.text = "%d/%d" % [current, max_val]
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
shield_bar.max_value = max_val
shield_bar.value = current
shield_label.text = "%d/%d" % [current, max_val]
func _on_entity_died(entity: Node) -> void:
if entity.name == "Player":

View File

@@ -2,3 +2,7 @@ extends CharacterBody3D
func _ready() -> void:
add_to_group("player")
if GameState.returning_from_dungeon:
GameState.restore_player(self)
global_position = GameState.portal_position + Vector3(0, 1, -5)
GameState.returning_from_dungeon = false

View File

@@ -8,7 +8,7 @@ var spawn_position: Vector3
@onready var player: CharacterBody3D = get_parent()
func _ready() -> void:
spawn_position = player.global_position
spawn_position = Vector3(0, 1, -5)
EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void:

34
scripts/portal/gate.gd Normal file
View File

@@ -0,0 +1,34 @@
extends StaticBody3D
@export var target_scene: String = "res://scenes/dungeon/dungeon.tscn"
@export var is_exit: bool = false
var active := false
func _ready() -> void:
if not is_exit:
if GameState.dungeon_cleared:
queue_free()
return
get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
else:
get_tree().create_timer(1.0).timeout.connect(func() -> void: active = true)
func _check_overlapping() -> void:
active = true
for body in $GateArea.get_overlapping_bodies():
_on_gate_area_body_entered(body)
func _on_gate_area_body_entered(body: Node3D) -> void:
if not active:
return
if body is CharacterBody3D and body.name == "Player":
GameState.save_player(body)
if is_exit:
GameState.returning_from_dungeon = true
else:
GameState.portal_position = global_position
call_deferred("_change_scene")
func _change_scene() -> void:
get_tree().change_scene_to_file(target_scene)

View File

@@ -0,0 +1 @@
uid://ctci5mc3cd2ck

View File

@@ -1,11 +1,22 @@
extends StaticBody3D
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
func _ready() -> void:
add_to_group("portals")
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if entity == self:
if entity != self:
return
var gate: Node3D = GATE_SCENE.instantiate()
gate.global_position = global_position
get_parent().add_child(gate)
var enemies := get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if is_instance_valid(enemy):
enemy.queue_free()
EventBus.portal_defeated.emit(self)
queue_free()
func _on_detection_area_body_entered(body: Node3D) -> void:

View File

@@ -0,0 +1,45 @@
extends Node
const PORTAL_SCENE: PackedScene = preload("res://scenes/portal/portal.tscn")
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
const SPAWN_INTERVAL := 30.0
const MAX_PORTALS := 3
const MIN_DISTANCE := 20.0
const MAX_DISTANCE := 40.0
var portals: Array[Node] = []
var timer := 0.0
func _ready() -> void:
if GameState.portal_position != Vector3.ZERO and not GameState.dungeon_cleared:
call_deferred("_restore_gate")
else:
if GameState.dungeon_cleared:
GameState.clear()
call_deferred("_spawn_portal")
func _restore_gate() -> void:
var gate: Node3D = GATE_SCENE.instantiate()
get_parent().add_child(gate)
gate.global_position = GameState.portal_position
func _process(delta: float) -> void:
timer += delta
if timer >= SPAWN_INTERVAL:
timer = 0.0
_cleanup_dead()
if portals.size() < MAX_PORTALS:
_spawn_portal()
func _spawn_portal() -> void:
var angle: float = randf() * TAU
var distance: float = randf_range(MIN_DISTANCE, MAX_DISTANCE)
var pos := Vector3(cos(angle) * distance, 0, sin(angle) * distance)
var portal: Node3D = PORTAL_SCENE.instantiate()
get_parent().add_child(portal)
portal.global_position = pos
portals.append(portal)
print("Portal gespawnt bei: %s" % pos)
func _cleanup_dead() -> void:
portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))

View File

@@ -0,0 +1 @@
uid://cskx6o07iukwh