From 04749104a04e68214ff388320fa5ed53dbb33dd3 Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Mon, 30 Mar 2026 22:56:58 +0200 Subject: [PATCH] update --- CLAUDE.md | 62 ++++-- infosammlung/Level 1.md | 46 +++++ infosammlung/Level 2.md | 60 ++++++ infosammlung/Level 3.md | 110 +++++++++++ infosammlung/Planung.md | 26 +++ infosammlung/Szenarien.md | 38 ++++ infosammlung/idden.md | 70 +++++++ infosammlung/story.md | 58 ++++++ plan.md | 252 +++++++++++++++--------- project.godot | 5 + resources/abilities/healer_aoe.tres | 13 ++ resources/abilities/healer_passive.tres | 13 ++ resources/abilities/healer_single.tres | 12 ++ resources/abilities/healer_ult.tres | 14 ++ resources/abilities/single_attack.tres | 2 +- resources/abilities/tank_aoe.tres | 12 ++ resources/abilities/tank_passive.tres | 13 ++ resources/abilities/tank_single.tres | 11 ++ resources/abilities/tank_ult.tres | 11 ++ resources/ability_sets/damage_set.tres | 11 +- resources/ability_sets/healer_set.tres | 13 +- resources/ability_sets/tank_set.tres | 12 +- resources/stats/boss_stats.tres | 11 ++ scenes/dungeon/dungeon.tscn | 157 +++++++++++++++ scenes/enemy/boss.tscn | 128 ++++++++++++ scenes/hud/hud.tscn | 24 +++ scenes/portal/gate.tscn | 36 ++++ scenes/world.tscn | 53 +++-- scripts/abilities/ability.gd | 57 +++++- scripts/abilities/ability_set.gd | 3 + scripts/components/health.gd | 9 + scripts/components/shield.gd | 20 ++ scripts/dungeon/dungeon_manager.gd | 16 ++ scripts/dungeon/dungeon_manager.gd.uid | 1 + scripts/enemy/boss.gd | 5 + scripts/enemy/boss.gd.uid | 1 + scripts/enemy/enemy_aggro.gd | 7 + scripts/event_bus.gd | 3 + scripts/game_state.gd | 42 ++++ scripts/game_state.gd.uid | 1 + scripts/player/combat.gd | 33 ++-- scripts/player/hud.gd | 4 + scripts/player/player.gd | 4 + scripts/player/respawn.gd | 2 +- scripts/portal/gate.gd | 34 ++++ scripts/portal/gate.gd.uid | 1 + scripts/portal/portal.gd | 15 +- scripts/world/portal_spawner.gd | 45 +++++ scripts/world/portal_spawner.gd.uid | 1 + 49 files changed, 1406 insertions(+), 171 deletions(-) create mode 100644 infosammlung/Level 1.md create mode 100644 infosammlung/Level 2.md create mode 100644 infosammlung/Level 3.md create mode 100644 infosammlung/Planung.md create mode 100644 infosammlung/Szenarien.md create mode 100644 infosammlung/idden.md create mode 100644 infosammlung/story.md create mode 100644 resources/abilities/healer_aoe.tres create mode 100644 resources/abilities/healer_passive.tres create mode 100644 resources/abilities/healer_single.tres create mode 100644 resources/abilities/healer_ult.tres create mode 100644 resources/abilities/tank_aoe.tres create mode 100644 resources/abilities/tank_passive.tres create mode 100644 resources/abilities/tank_single.tres create mode 100644 resources/abilities/tank_ult.tres create mode 100644 resources/stats/boss_stats.tres create mode 100644 scenes/dungeon/dungeon.tscn create mode 100644 scenes/enemy/boss.tscn create mode 100644 scenes/portal/gate.tscn create mode 100644 scripts/dungeon/dungeon_manager.gd create mode 100644 scripts/dungeon/dungeon_manager.gd.uid create mode 100644 scripts/enemy/boss.gd create mode 100644 scripts/enemy/boss.gd.uid create mode 100644 scripts/game_state.gd create mode 100644 scripts/game_state.gd.uid create mode 100644 scripts/portal/gate.gd create mode 100644 scripts/portal/gate.gd.uid create mode 100644 scripts/world/portal_spawner.gd create mode 100644 scripts/world/portal_spawner.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index 5fc759e..eb53165 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/infosammlung/Level 1.md b/infosammlung/Level 1.md new file mode 100644 index 0000000..6715ad4 --- /dev/null +++ b/infosammlung/Level 1.md @@ -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 \ No newline at end of file diff --git a/infosammlung/Level 2.md b/infosammlung/Level 2.md new file mode 100644 index 0000000..77ca549 --- /dev/null +++ b/infosammlung/Level 2.md @@ -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 \ No newline at end of file diff --git a/infosammlung/Level 3.md b/infosammlung/Level 3.md new file mode 100644 index 0000000..ac9bf2c --- /dev/null +++ b/infosammlung/Level 3.md @@ -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 \ No newline at end of file diff --git a/infosammlung/Planung.md b/infosammlung/Planung.md new file mode 100644 index 0000000..53d3b28 --- /dev/null +++ b/infosammlung/Planung.md @@ -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 diff --git a/infosammlung/Szenarien.md b/infosammlung/Szenarien.md new file mode 100644 index 0000000..6188883 --- /dev/null +++ b/infosammlung/Szenarien.md @@ -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 --> ... \ No newline at end of file diff --git a/infosammlung/idden.md b/infosammlung/idden.md new file mode 100644 index 0000000..3b315af --- /dev/null +++ b/infosammlung/idden.md @@ -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 \ No newline at end of file diff --git a/infosammlung/story.md b/infosammlung/story.md new file mode 100644 index 0000000..bc92380 --- /dev/null +++ b/infosammlung/story.md @@ -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 + diff --git a/plan.md b/plan.md index 668481f..6925467 100644 --- a/plan.md +++ b/plan.md @@ -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,74 +100,119 @@ - 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 +- enemy_stats.tres — health=100, regen=0, shield=50, delay=3s, regen_time=5s +- Aggro-Regeln: + - Schaden = Aggro (1:1) + - Heilung = Aggro (0.5x) + - Tank = Aggro-Multiplikator (2x) + - Aggro verfällt -1/s + - Spieler im Portal-Radius: Aggro bleibt bei mindestens 1 + - Außerhalb 10m Portal-Radius: Aggro verfällt 1% * 2 je s (1%, 2%, 4%, 8%, ...) + - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert 10% Leben/s bis 100%, dann 1%/s + - Bei Spieler-Tod → Aggro auf 0 -## Components (scripts/components/, wiederverwendbar) +## 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) -- 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 -- Schaden = Aggro (1:1) -- Heilung = Aggro (0.5x) -- Tank = Aggro-Multiplikator (2x) -- Aggro verfällt -1/s -- Spieler im Portal-Radius: Aggro bleibt bei mindestens 1 -- Außerhalb 10m Portal-Radius: Aggro verfällt 1% * 2 je s (1%, 2%, 4%, 8%, ...) -- 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 +## 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 -- 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_died(entity) — Entity ist gestorben -- shield_broken(entity) — Schild ist auf 0 gefallen -- shield_regenerated(entity) — Schild ist wieder voll -- 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 -- portal_spawn(portal, enemies) — Portal hat Gegner gespawnt +- 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 + - respawn_tick(timer) — Respawn-Countdown Update + - 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 diff --git a/project.godot b/project.godot index a1b7f1b..7b907ed 100644 --- a/project.godot +++ b/project.godot @@ -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] diff --git a/resources/abilities/healer_aoe.tres b/resources/abilities/healer_aoe.tres new file mode 100644 index 0000000..e01bca5 --- /dev/null +++ b/resources/abilities/healer_aoe.tres @@ -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 diff --git a/resources/abilities/healer_passive.tres b/resources/abilities/healer_passive.tres new file mode 100644 index 0000000..0fb8a69 --- /dev/null +++ b/resources/abilities/healer_passive.tres @@ -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" diff --git a/resources/abilities/healer_single.tres b/resources/abilities/healer_single.tres new file mode 100644 index 0000000..be57f1f --- /dev/null +++ b/resources/abilities/healer_single.tres @@ -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 diff --git a/resources/abilities/healer_ult.tres b/resources/abilities/healer_ult.tres new file mode 100644 index 0000000..fecc548 --- /dev/null +++ b/resources/abilities/healer_ult.tres @@ -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 diff --git a/resources/abilities/single_attack.tres b/resources/abilities/single_attack.tres index d066242..1a31d66 100644 --- a/resources/abilities/single_attack.tres +++ b/resources/abilities/single_attack.tres @@ -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" diff --git a/resources/abilities/tank_aoe.tres b/resources/abilities/tank_aoe.tres new file mode 100644 index 0000000..aa134cd --- /dev/null +++ b/resources/abilities/tank_aoe.tres @@ -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" diff --git a/resources/abilities/tank_passive.tres b/resources/abilities/tank_passive.tres new file mode 100644 index 0000000..d8deb30 --- /dev/null +++ b/resources/abilities/tank_passive.tres @@ -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" diff --git a/resources/abilities/tank_single.tres b/resources/abilities/tank_single.tres new file mode 100644 index 0000000..2888433 --- /dev/null +++ b/resources/abilities/tank_single.tres @@ -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" diff --git a/resources/abilities/tank_ult.tres b/resources/abilities/tank_ult.tres new file mode 100644 index 0000000..d2c3a68 --- /dev/null +++ b/resources/abilities/tank_ult.tres @@ -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" diff --git a/resources/ability_sets/damage_set.tres b/resources/ability_sets/damage_set.tres index 339a652..02efc47 100644 --- a/resources/ability_sets/damage_set.tres +++ b/resources/ability_sets/damage_set.tres @@ -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 diff --git a/resources/ability_sets/healer_set.tres b/resources/ability_sets/healer_set.tres index 6e67c4a..e1d351b 100644 --- a/resources/ability_sets/healer_set.tres +++ b/resources/ability_sets/healer_set.tres @@ -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 diff --git a/resources/ability_sets/tank_set.tres b/resources/ability_sets/tank_set.tres index 6705b44..2ae95cf 100644 --- a/resources/ability_sets/tank_set.tres +++ b/resources/ability_sets/tank_set.tres @@ -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 diff --git a/resources/stats/boss_stats.tres b/resources/stats/boss_stats.tres new file mode 100644 index 0000000..9b27049 --- /dev/null +++ b/resources/stats/boss_stats.tres @@ -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 diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn new file mode 100644 index 0000000..bb4c764 --- /dev/null +++ b/scenes/dungeon/dungeon.tscn @@ -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") diff --git a/scenes/enemy/boss.tscn b/scenes/enemy/boss.tscn new file mode 100644 index 0000000..3d7e9d6 --- /dev/null +++ b/scenes/enemy/boss.tscn @@ -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"] diff --git a/scenes/hud/hud.tscn b/scenes/hud/hud.tscn index c385404..c2689b6 100644 --- a/scenes/hud/hud.tscn +++ b/scenes/hud/hud.tscn @@ -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 diff --git a/scenes/portal/gate.tscn b/scenes/portal/gate.tscn new file mode 100644 index 0000000..9d76e8a --- /dev/null +++ b/scenes/portal/gate.tscn @@ -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"] diff --git a/scenes/world.tscn b/scenes/world.tscn index 5af9edb..aa8c6b3 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -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") diff --git a/scripts/abilities/ability.gd b/scripts/abilities/ability.gd index 4275f4a..05e1fd3 100644 --- a/scripts/abilities/ability.gd +++ b/scripts/abilities/ability.gd @@ -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: - shield.current_shield = shield.max_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) diff --git a/scripts/abilities/ability_set.gd b/scripts/abilities/ability_set.gd index 58ab09e..1e16b78 100644 --- a/scripts/abilities/ability_set.gd +++ b/scripts/abilities/ability_set.gd @@ -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 diff --git a/scripts/components/health.gd b/scripts/components/health.gd index 62721d8..4f2c173 100644 --- a/scripts/components/health.gd +++ b/scripts/components/health.gd @@ -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) diff --git a/scripts/components/shield.gd b/scripts/components/shield.gd index 2b55453..c3a6e57 100644 --- a/scripts/components/shield.gd +++ b/scripts/components/shield.gd @@ -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 diff --git a/scripts/dungeon/dungeon_manager.gd b/scripts/dungeon/dungeon_manager.gd new file mode 100644 index 0000000..68bd460 --- /dev/null +++ b/scripts/dungeon/dungeon_manager.gd @@ -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") diff --git a/scripts/dungeon/dungeon_manager.gd.uid b/scripts/dungeon/dungeon_manager.gd.uid new file mode 100644 index 0000000..0ee401e --- /dev/null +++ b/scripts/dungeon/dungeon_manager.gd.uid @@ -0,0 +1 @@ +uid://drn4h1lxx5t1j diff --git a/scripts/enemy/boss.gd b/scripts/enemy/boss.gd new file mode 100644 index 0000000..ac8a2c3 --- /dev/null +++ b/scripts/enemy/boss.gd @@ -0,0 +1,5 @@ +extends "res://scripts/enemy/enemy.gd" + +func _ready() -> void: + super._ready() + add_to_group("boss") diff --git a/scripts/enemy/boss.gd.uid b/scripts/enemy/boss.gd.uid new file mode 100644 index 0000000..0397868 --- /dev/null +++ b/scripts/enemy/boss.gd.uid @@ -0,0 +1 @@ +uid://bkehq3dqyp2yd diff --git a/scripts/enemy/enemy_aggro.gd b/scripts/enemy/enemy_aggro.gd index 2f1160c..1440971 100644 --- a/scripts/enemy/enemy_aggro.gd +++ b/scripts/enemy/enemy_aggro.gd @@ -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) diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index 5c45ed6..7ed5182 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -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() diff --git a/scripts/game_state.gd b/scripts/game_state.gd new file mode 100644 index 0000000..356bbbe --- /dev/null +++ b/scripts/game_state.gd @@ -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 diff --git a/scripts/game_state.gd.uid b/scripts/game_state.gd.uid new file mode 100644 index 0000000..fee2af4 --- /dev/null +++ b/scripts/game_state.gd.uid @@ -0,0 +1 @@ +uid://cp2vadwcd12sm diff --git a/scripts/player/combat.gd b/scripts/player/combat.gd index f735d80..5fe5f67 100644 --- a/scripts/player/combat.gd +++ b/scripts/player/combat.gd @@ -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) - EventBus.damage_requested.emit(player, targeting.current_target, dmg) - print("AA: %s Schaden an %s" % [dmg, targeting.current_target.name]) + 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, 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() diff --git a/scripts/player/hud.gd b/scripts/player/hud.gd index 2a9d20e..162839b 100644 --- a/scripts/player/hud.gd +++ b/scripts/player/hud.gd @@ -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": diff --git a/scripts/player/player.gd b/scripts/player/player.gd index dfc0d8c..32859a8 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -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 diff --git a/scripts/player/respawn.gd b/scripts/player/respawn.gd index 0720cb4..c63ac92 100644 --- a/scripts/player/respawn.gd +++ b/scripts/player/respawn.gd @@ -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: diff --git a/scripts/portal/gate.gd b/scripts/portal/gate.gd new file mode 100644 index 0000000..5b0c0dd --- /dev/null +++ b/scripts/portal/gate.gd @@ -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) diff --git a/scripts/portal/gate.gd.uid b/scripts/portal/gate.gd.uid new file mode 100644 index 0000000..76d88b1 --- /dev/null +++ b/scripts/portal/gate.gd.uid @@ -0,0 +1 @@ +uid://ctci5mc3cd2ck diff --git a/scripts/portal/portal.gd b/scripts/portal/portal.gd index a41f7b9..5e548de 100644 --- a/scripts/portal/portal.gd +++ b/scripts/portal/portal.gd @@ -1,12 +1,23 @@ 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: - queue_free() + 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: if body is CharacterBody3D and body.name == "Player": diff --git a/scripts/world/portal_spawner.gd b/scripts/world/portal_spawner.gd new file mode 100644 index 0000000..7f22918 --- /dev/null +++ b/scripts/world/portal_spawner.gd @@ -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)) diff --git a/scripts/world/portal_spawner.gd.uid b/scripts/world/portal_spawner.gd.uid new file mode 100644 index 0000000..5b5028e --- /dev/null +++ b/scripts/world/portal_spawner.gd.uid @@ -0,0 +1 @@ +uid://cskx6o07iukwh