Compare commits

..

6 Commits

Author SHA1 Message Date
52ad83a96d update! 2026-05-14 19:11:10 +02:00
Marek Le
2d4002bd3f refactor 2026-05-09 23:37:26 +02:00
Marek Le
6d28b04c12 update 2026-04-25 05:15:43 +02:00
Marek Le
087a5ec8cc update 2026-04-19 19:21:54 +02:00
Marek Lenczewski
4b0f82c1de asset vibe 2026-04-16 18:02:03 +02:00
Marek Lenczewski
f21e30eb55 prototype vibe 2026-04-16 17:20:57 +02:00
288 changed files with 6275 additions and 3503 deletions

152
CLAUDE.md
View File

@@ -1,92 +1,98 @@
# MMO Projekt
## Überblick
Community-MMO in Godot 4.6 (Forward+, Jolt Physics). Ziel: Einsamkeit bekämpfen durch geographische Nähe der Spieler. Stilles Designprinzip — nicht als "Anti-Einsamkeits-Spiel" vermarktet.
Community-MMO in Godot 4.6 (Forward+, Jolt Physics). Vision in `doc/_core.md`, Features in `doc/features.md`.
## Sprache
Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare nur wo nötig.
## Code-Style
- GDScript mit 2 Spaces Einrückung (Godot Editor auf Spaces/2 eingestellt)
- Explizite Typen bei Variablen die von Variant-Funktionen kommen (z.B. `var dist: float = ...` statt `var dist := ...` bei `distance_to()`, `min()`, `get_node_or_null()`)
- Keine Debug-Prints im finalen Code (nur temporär zum Testen)
- GDScript mit 2 Spaces Einrückung
- Explizite Typen wo der Inferenzer scheitert (Ergebnisse von Variant-Funktionen, Helper-Returns die Node sein könnten)
- Keine Debug-Prints im finalen Code
- Keine emoji
## Architektur
- **Stats (Model)**: Autoload, zentrale Datenhaltung aller Entity-Attribute. Basiswerte aus Resources.
- **Systeme (Controller)**: Scene-Nodes in world.tscn/dungeon.tscn, lesen/schreiben über Stats.
- **Szenen (Views)**: Rendern, Input senden, Events empfangen. Kein Gameplay-State.
- **EventBus (Signals)**: Autoload, Kommunikation zwischen Szenen und Systemen.
- **Event-Flow**: Szene → Input → EventBus → System → Stats → EventBus → Szene
- **Zwischen Szenen**: Kommunikation über EventBus. Szenen kennen sich nicht.
- **Innerhalb einer Szene**: Zugriff auf Geschwister-Nodes erlaubt.
- **Autoloads**: EventBus (Signals), Stats (Entity-Daten), GameState (Szene + Position)
- **Gruppen**: "player", "enemies", "portals", "boss", "cooldown_system"
- **Resources** für Basiswerte (Stats, Abilities), **Stats Autoload** für Laufzeitwerte
## Projektstruktur
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
- `scenes/` — Darstellung + Input
- `player/` — Spieler + player_stats + role/ (Rollen + Abilities)
- `enemy/` — Gegner + enemy_stats + boss/ (Boss + boss_stats)
- `portal/` — Portal + Gate + portal_stats
- `dungeon/` — Dungeon + dungeon_manager
- `hud/` — HUD
- `world/` — Hauptszene + portal_spawner
- `effect_icon_factory.gd` — Shared Utility (Effekt-Icons)
- `healthbar*.gd` — Healthbar-Komponenten (health, shield, status, effects)
- `systems/` — Spiellogik
- 12 Systeme (health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, aura, buff_calc)
- `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten)
- `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config
- `autoloads/` — Globaler Zustand
- event_bus, game_state
- `stats/` — stats + base_stats
### Autoloads (`autoloads/`)
- `EventBus` — alle Signals (Combat, Wave, Inventory, Dialog, Chat, …)
- `Net` — ENet P2P Multiplayer-Manager (Singleplayer = OfflineMultiplayerPeer)
- `Stats` — zentrale Registry: `register(entity, base_resource)` / `get_stat` / `set_stat`
- `GameState` — Wave#, Scene-Pfade, Run-Seed, Pause, Dungeon-Seed
- `SaveLoad` — JSON-basierte Persistenz (Stub für später)
## Planungsdokument
`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.
### Resources (`resources/`)
- `stats/` — BaseStats + Player/Enemy/Boss/Portal/Gate/Village/Building Stats
- `abilities/` — Ability + AbilitySet (3 Klassen × 5 Abilities, im RoleSystem in Code aufgebaut)
- `items/` — Item, Recipe (Crafting in CraftingSystem in Code)
- `buildings/` — Building Resource (Blueprints in BuildingSystem in Code)
- `npcs/` — NpcProfile (Lore + Personality für Ollama)
- `effects/` — Effect Resource + Element (NONE, FIRE)
## Core Loop
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 → 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
### Szenen (`scenes/`)
- `menu/` — main_menu, lobby, options_menu
- `world/` — world.tscn (Dorf-Welt mit Village + alle Systeme als Children)
- `dungeon/` — dungeon.tscn (procedural generation via dungeon_generator.gd)
- `entities/` — player, enemy, gate, portal, building, loot, npc, village
- `hud/` — hud.tscn (alles-in-einem: vitals, abilities, chat, minimap, inventory, crafting, build, dialog, map, pause, game over)
## Kampfsystem
- 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
- Heilung: Heiler heilt sich selbst (Singleplayer), is_heal Flag auf Ability
- Passive: Schadens-Boost (D), Schild-Boost (T), Heal-Boost (H) — je 50%, als Aura (50m Radius)
- Targeting: Klick, TAB (cyclet "enemies" + "portals"), Auto-Target (Gegner > Portal)
- Aggro-System: 1:1 Schaden, Tank 2x, Heilung 0.5x, verfällt -1/s, exponentiell außerhalb Portal-Radius
### Systeme (`systems/`)
Alle als Children unter `World/Systems` und `Dungeon/Systems` instanziert:
- Combat: HealthSystem, ShieldSystem, RespawnSystem, CooldownSystem, AbilitySystem, AutoAttackSystem
- Effects: EffectSystem, ElementSystem, AggroSystem, RoleSystem
- World: SpawnSystem, WaveSystem, InvasionSystem, XpSystem, LootSystem
- Player: InventorySystem, CraftingSystem, BuildingSystem
- Social: NpcSystem, DialogSystem (Ollama HTTP), ChatSystem, MapSystem, AudioSystem
## Rollen
- Tank (T), Schaden (D), Heiler (H) — wechselbar mit ALT+1/2/3
- 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
### Multiplayer-Pattern
- ENet P2P, Host-Authoritative
- Singleplayer = `Net.host_singleplayer()` (OfflineMultiplayerPeer)
- Player-Entities tragen Authority des jeweiligen Peers (`_enter_tree() → set_multiplayer_authority(name.to_int())`)
- Stats/Damage/Spawn/Wave/XP/Inventory laufen nur am Host (`if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: return`)
- Client-Aktionen via RPC zum Host: `_request_*.rpc_id(1, ...)` → Host validiert + sync via `MultiplayerSpawner` und `MultiplayerSynchronizer`
- Sync-Felder pro Entity über `MultiplayerSynchronizer` mit `SceneReplicationConfig`
## Effekte & Elemente
- **EffectSystem**: Verwaltet Buffs, Debuffs, Auras auf allen Entities. Kein Stacking (gleicher Name → Refresh)
- **Effect Resource**: effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration (-1=permanent), is_multiplier, aura_radius, tick_interval, element
- **Auras**: Passive-Abilities werden als AURA-Effekte erstellt. Propagieren Buffs auf Spieler im aura_radius. Verlässt man Radius → Buff sofort weg
- **ElementSystem**: Verwaltet Elementar-Zustände auf Enemies + Portalen. Aktuell: Feuer (DoT 3/Tick, 2s Interval, 6s)
- **Abilities**: `element` Feld (0=NONE, 1=FIRE). Schadens-Abilities der Damage-Rolle haben element=1 (Feuer)
- **Effekt-Icons**: Auf Healthbars (Enemies, Boss, Portal) und Spieler-HUD. Aura=blau, Buff=grün, Debuff=rot
### Input-Map
- WASD + Space: Move + Jump
- 14: Abilities (im Build-Mode: Bauteil-Auswahl)
- ALT+1/2/3: Tank/Damage/Healer
- Tab: Cycle target | LMB: Click target | RMB: Camera drag (capture mouse)
- I: Inventory | C: Crafting | B: Build mode | M: Map | Y: Chat | E: Interact (NPC)
- R: Rotate building preview
- Escape: Pause / Cancel UI
## Szenenwechsel
- Stats Autoload cached Spieler-Werte automatisch bei Szenenwechsel
- GameState speichert Rolle + Position
- Gate (Eingang): save_player → Dungeon laden
- Gate (Exit): returning_from_dungeon → Welt laden, Spieler bei Gate-Position
- Boss-Tod: dungeon_cleared → Welt laden, Cache geleert, Spieler bei Taverne mit vollen HP
### Dialog (Ollama)
- HTTP `localhost:11434/api/generate`, Modell `mistral-nemo`
- Pro NPC: System-Prompt aus `lore` + `personality`
- Fallback: `npc.fallback_text` wenn Ollama nicht erreichbar — Spiel bleibt voll spielbar
### Bauen
- Grid-Snap 1m, 4 Bauteile (Floor, Wall, Door, Roof)
- LMB im Build-Mode platziert, MMB entfernt
- Material-Verbrauch aus Crafting-Items (alle aus `wood`, das aus `essence` gecrafted wird)
- Persistenz via Host (Buildings sind echte Nodes unter `EntityRoot/Buildings`, MultiplayerSpawner repliziert)
### Wave-Loop
1. WaveSystem startet 10-min Timer, spawnt 3 normale + 1 rotes Gate
2. Gate stirbt → spawnt Portal an gleicher Position
3. Spieler betritt Portal → Dungeon (procedural, 58 Räume + Boss)
4. Boss-Tod → Welt-Rückkehr; rotes Portal → Wave++ + alle Stats skalieren
5. Timer 0 ohne rotes Portal → Invasion (8+wave×2 Mobs zur Village)
6. Village 0 HP → Game Over → Hauptmenü
## Workflow mit dem User
- **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren
- **Modularer Aufbau**Jede Szene/Skript hat eine Aufgabe
- **Schrittweise** — Erst planen, dann Code zeigen, dann implementieren
- **Godot-Eigenheiten**: Nach Änderungen an Autoloads/Scripts Godot neu starten. Godot überschreibt .tscn beim Speichern — Szene im Editor schließen vor externen Edits.
- Erst planen (`doc/` lesen), dann implementieren
- Schrittweise, modulares Aufbauennicht alles auf einmal umbauen
- Nach Änderungen: `godot-4 --headless --quit-after 60 res://scenes/world/world.tscn` um Parse-/Runtime-Fehler zu sehen
- `doc/` nicht anfassen — das ist die User-Spec
## Smoke-Test
- `godot-4 --headless --import` → Kein Error
- `godot-4 --headless --quit-after 600 res://scenes/world/world.tscn` → Kein Error
- `godot-4 --headless --quit-after 600 res://scenes/dungeon/dungeon.tscn` → Kein Error
## Bekannte Lücken / TODOs
- Ollama muss installiert werden (Modell `mistral-nemo` erwartet) — Stub-Fallback funktioniert sonst
- Save/Load Slots im Hauptmenü (Autoload existiert, UI fehlt)
- Multiplayer Late-Join: Building-/Wave-State wird beim spätem Beitritt nicht synchronisiert
- Damage Numbers / Hit FX / Particle Effects fehlen (rein Polish)

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://d4a76sj0xk578"
path="res://.godot/imported/battle.wav-74e134d2675fd72e2f3b1b22f3e2be00.sample"
[deps]
source_file="res://assets/audio/music/battle.wav"
dest_files=["res://.godot/imported/battle.wav-74e134d2675fd72e2f3b1b22f3e2be00.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://clvph60pdl81v"
path="res://.godot/imported/invasion.wav-6117678cf33bae18ebd1655477798610.sample"
[deps]
source_file="res://assets/audio/music/invasion.wav"
dest_files=["res://.godot/imported/invasion.wav-6117678cf33bae18ebd1655477798610.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dnowuon22316o"
path="res://.godot/imported/tavern.wav-8d46496f3ea930a74a1080b53ceb6a0e.sample"
[deps]
source_file="res://assets/audio/music/tavern.wav"
dest_files=["res://.godot/imported/tavern.wav-8d46496f3ea930a74a1080b53ceb6a0e.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://crpv7tej1lwnn"
path="res://.godot/imported/ability_cast.wav-05d75ac0fb52a99d863b460bd374fd73.sample"
[deps]
source_file="res://assets/audio/sfx/ability_cast.wav"
dest_files=["res://.godot/imported/ability_cast.wav-05d75ac0fb52a99d863b460bd374fd73.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/audio/sfx/death.wav Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dmmeubtnbibaj"
path="res://.godot/imported/death.wav-eb8bf206799fefce7cf26366434348b8.sample"
[deps]
source_file="res://assets/audio/sfx/death.wav"
dest_files=["res://.godot/imported/death.wav-eb8bf206799fefce7cf26366434348b8.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/audio/sfx/hit.wav Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://blv6fecsx5wax"
path="res://.godot/imported/hit.wav-27e178036f6cee6545e9f025a3865a36.sample"
[deps]
source_file="res://assets/audio/sfx/hit.wav"
dest_files=["res://.godot/imported/hit.wav-27e178036f6cee6545e9f025a3865a36.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cys5bjcvl3d1q"
path="res://.godot/imported/invasion_alarm.wav-f4375961fdd02f77d27cc495cdccde5c.sample"
[deps]
source_file="res://assets/audio/sfx/invasion_alarm.wav"
dest_files=["res://.godot/imported/invasion_alarm.wav-f4375961fdd02f77d27cc495cdccde5c.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://2ogpkwinaq32"
path="res://.godot/imported/level_up.wav-60e30bfe8ab247d27e7615e99e00b8f1.sample"
[deps]
source_file="res://assets/audio/sfx/level_up.wav"
dest_files=["res://.godot/imported/level_up.wav-60e30bfe8ab247d27e7615e99e00b8f1.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c6us2m0yer33r"
path="res://.godot/imported/portal_spawn.wav-7a2c81bb1bec2cdea2b6aa74f4884f93.sample"
[deps]
source_file="res://assets/audio/sfx/portal_spawn.wav"
dest_files=["res://.godot/imported/portal_spawn.wav-7a2c81bb1bec2cdea2b6aa74f4884f93.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dcfon26db14hk"
path="res://.godot/imported/tavern_damage.wav-1e664835ae36452ac3c6f2da885f2ce2.sample"
[deps]
source_file="res://assets/audio/sfx/tavern_damage.wav"
dest_files=["res://.godot/imported/tavern_damage.wav-1e664835ae36452ac3c6f2da885f2ce2.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,136 @@
{
"asset" : {
"generator" : "Khronos glTF Blender I/O v3.4.50",
"version" : "2.0"
},
"scene" : 0,
"scenes" : [
{
"name" : "Scene",
"nodes" : [
0
]
}
],
"nodes" : [
{
"mesh" : 0,
"name" : "building_home_A_blue"
}
],
"materials" : [
{
"name" : "hexagons_medieval",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.5
}
}
],
"meshes" : [
{
"name" : "building_home_A_blue",
"primitives" : [
{
"attributes" : {
"POSITION" : 0,
"TEXCOORD_0" : 1,
"NORMAL" : 2
},
"indices" : 3,
"material" : 0
}
]
}
],
"textures" : [
{
"sampler" : 0,
"source" : 0
}
],
"images" : [
{
"mimeType" : "image/png",
"name" : "hexagons_medieval",
"uri" : "hexagons_medieval.png"
}
],
"accessors" : [
{
"bufferView" : 0,
"componentType" : 5126,
"count" : 1540,
"max" : [
0.3959798812866211,
0.9300001263618469,
0.385000079870224
],
"min" : [
-0.39597994089126587,
-2.086162709247219e-08,
-0.46865156292915344
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 1540,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 1540,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 3033,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 18480,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 12320,
"byteOffset" : 18480,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 18480,
"byteOffset" : 30800,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 6066,
"byteOffset" : 49280,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 55348,
"uri" : "building_home_A_blue.bin"
}
]
}

View File

@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://digqjcku7u6jw"
path="res://.godot/imported/building_home_A_blue.gltf-3d8bffb321af199d2bf47d5e9358bb70.scn"
[deps]
source_file="res://assets/models/buildings/building_home_A_blue.gltf"
dest_files=["res://.godot/imported/building_home_A_blue.gltf-3d8bffb321af199d2bf47d5e9358bb70.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,41 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ikdyfxy2qnbi"
path.s3tc="res://.godot/imported/hexagons_medieval.png-ea132e7b0754db3463363d12b2f4f568.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/models/buildings/hexagons_medieval.png"
dest_files=["res://.godot/imported/hexagons_medieval.png-ea132e7b0754db3463363d12b2f4f568.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View File

@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c7dnarple2ye6"
path="res://.godot/imported/Knight.glb-f3b84697960dc30f97048f59f901a6e9.scn"
[deps]
source_file="res://assets/models/characters/Knight.glb"
dest_files=["res://.godot/imported/Knight.glb-f3b84697960dc30f97048f59f901a6e9.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://n1j7b5ws4ee0"
path.s3tc="res://.godot/imported/Knight_knight_texture.png-fbfd6db275b893706e62960fa115ac3a.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "af4707185c3e9844f7ed166c1a2e24f8"
}
[deps]
source_file="res://assets/models/characters/Knight_knight_texture.png"
dest_files=["res://.godot/imported/Knight_knight_texture.png-fbfd6db275b893706e62960fa115ac3a.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View File

@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://7sbngaphdxht"
path="res://.godot/imported/Skeleton_Mage.glb-dfda87ec6175e75ab440ca2eec40520e.scn"
[deps]
source_file="res://assets/models/characters/Skeleton_Mage.glb"
dest_files=["res://.godot/imported/Skeleton_Mage.glb-dfda87ec6175e75ab440ca2eec40520e.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wjwg0opxh8we"
path.s3tc="res://.godot/imported/Skeleton_Mage_skeleton_texture.png-ddebdfcfc8bd13280cab68f2d8b07b2b.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "661544b1e8ba10b5a0c98b638eae6d0b"
}
[deps]
source_file="res://assets/models/characters/Skeleton_Mage_skeleton_texture.png"
dest_files=["res://.godot/imported/Skeleton_Mage_skeleton_texture.png-ddebdfcfc8bd13280cab68f2d8b07b2b.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View File

@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dkn5fqfvkqog5"
path="res://.godot/imported/Skeleton_Minion.glb-71f08c897190630c8e5ae9e5beda9052.scn"
[deps]
source_file="res://assets/models/characters/Skeleton_Minion.glb"
dest_files=["res://.godot/imported/Skeleton_Minion.glb-71f08c897190630c8e5ae9e5beda9052.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://caa8qxgnpgv61"
path.s3tc="res://.godot/imported/Skeleton_Minion_skeleton_texture.png-9dcb8889e4229fae505e24dc5a110852.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "661544b1e8ba10b5a0c98b638eae6d0b"
}
[deps]
source_file="res://assets/models/characters/Skeleton_Minion_skeleton_texture.png"
dest_files=["res://.godot/imported/Skeleton_Minion_skeleton_texture.png-9dcb8889e4229fae505e24dc5a110852.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View File

@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cjgucsfp8qn1m"
path="res://.godot/imported/Skeleton_Warrior.glb-87a31fa24e48c23f04684ddfdaed2d10.scn"
[deps]
source_file="res://assets/models/characters/Skeleton_Warrior.glb"
dest_files=["res://.godot/imported/Skeleton_Warrior.glb-87a31fa24e48c23f04684ddfdaed2d10.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvee751avas5j"
path.s3tc="res://.godot/imported/Skeleton_Warrior_skeleton_texture.png-66c57b76e95467b6ce503b8825ef03ca.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "661544b1e8ba10b5a0c98b638eae6d0b"
}
[deps]
source_file="res://assets/models/characters/Skeleton_Warrior_skeleton_texture.png"
dest_files=["res://.godot/imported/Skeleton_Warrior_skeleton_texture.png-66c57b76e95467b6ce503b8825ef03ca.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View File

@@ -1,64 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats) -> void:
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"health_regen": base.health_regen,
"shield": base.max_shield,
"max_shield": base.max_shield,
"shield_regen_delay": base.shield_regen_delay,
"shield_regen_time": base.shield_regen_time,
"shield_regen_timer": 0.0,
"alive": true,
"buff_damage": 1.0,
"buff_heal": 1.0,
"buff_shield": 1.0,
"state": 0,
"target": null,
"spawn_position": Vector3.ZERO,
"portal": null,
"attack_timer": 0.0,
}
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func get_base(entity: Node) -> EnemyStats:
if entity in entities:
return entities[entity]["base"]
return null
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)
func set_shield(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["shield"] = value
var max_shield: float = entities[entity]["max_shield"]
EventBus.shield_changed.emit(entity, value, max_shield)

View File

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

View File

@@ -1,64 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats) -> void:
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"health_regen": base.health_regen,
"shield": base.max_shield,
"max_shield": base.max_shield,
"shield_regen_delay": base.shield_regen_delay,
"shield_regen_time": base.shield_regen_time,
"shield_regen_timer": 0.0,
"alive": true,
"buff_damage": 1.0,
"buff_heal": 1.0,
"buff_shield": 1.0,
"state": 0,
"target": null,
"spawn_position": Vector3.ZERO,
"portal": null,
"attack_timer": 0.0,
}
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func get_base(entity: Node) -> EnemyStats:
if entity in entities:
return entities[entity]["base"]
return null
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)
func set_shield(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["shield"] = value
var max_shield: float = entities[entity]["max_shield"]
EventBus.shield_changed.emit(entity, value, max_shield)

View File

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

View File

@@ -1,52 +1,60 @@
extends Node
# Intentionen (Input → System)
signal ability_use(player, ability_index)
signal role_change_requested(player, role)
signal target_requested(player, target)
signal enemy_detected(enemy, player)
signal enemy_lost(enemy, player)
signal portal_entered(portal, player)
signal entity_registered(entity: Node)
signal entity_deregistered(entity: Node)
# Kampf
signal attack_executed(attacker, position, direction, damage)
signal damage_dealt(attacker, target, damage)
signal damage_requested(attacker, target, amount)
signal heal_requested(healer, target, amount)
signal damage_requested(attacker: Node, target: Node, amount: float, element: int)
signal heal_requested(healer: Node, target: Node, amount: float)
signal damage_dealt(attacker: Node, target: Node, amount: float)
signal entity_died(entity: Node)
signal entity_respawned(entity: Node)
signal health_changed(entity: Node, current: float, max: float)
signal shield_changed(entity: Node, current: float, max: float)
signal shield_broken(entity: Node)
# Entity
signal entity_died(entity)
signal health_changed(entity, current, max_val)
signal shield_changed(entity, current, max_val)
signal shield_broken(entity)
signal shield_regenerated(entity)
signal ability_use_requested(player: Node, ability_index: int)
signal ability_used(player: Node, ability_index: int, ability: Resource)
signal cooldown_tick(entity: Node, cds: PackedFloat32Array, max_cds: PackedFloat32Array, gcd: float)
signal role_changed(player: Node, role: int)
signal target_changed(player: Node, target: Node)
# Spieler
signal target_changed(player, target)
signal player_respawned(player)
signal role_changed(player, role_type)
signal respawn_tick(timer)
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
signal effect_applied(target: Node, effect: Resource, source: Node)
signal effect_expired(target: Node, effect: Resource)
signal element_applied(target: Node, element: int)
signal buff_changed(entity: Node, stat: StringName, value: float)
# Buff
signal buff_changed(entity, stat, value)
signal enemy_detected(enemy: Node, player: Node)
signal enemy_lost(enemy: Node, player: Node)
signal enemy_engaged(enemy: Node, target: Node)
# Gegner
signal enemy_engaged(enemy, target)
signal gate_destroyed(gate: Node)
signal portal_spawned(portal: Node)
signal portal_entered(portal: Node, player: Node)
signal dungeon_cleared(seed: int)
signal boss_defeated(boss: Node)
# Portal
signal portal_spawn(portal, enemies)
signal portal_defeated(portal)
signal loot_dropped(items: Array, position: Vector3)
signal item_picked_up(player: Node, item: Resource)
signal inventory_changed(player: Node)
signal item_crafted(player: Node, item: Resource)
signal building_placed(building: Node)
signal building_removed(building: Node)
# Dungeon
signal dungeon_cleared()
signal wave_started(wave_number: int)
signal wave_timer_tick(seconds_remaining: float)
signal wave_ended(wave_number: int, success: bool)
signal invasion_started()
signal invasion_ended(success: bool)
signal village_damaged(current: float, max: float)
signal village_destroyed()
signal game_over()
signal run_started(wave_number: int)
# Effects
signal effect_requested(target, effect, source)
signal effect_applied(target, effect)
signal effect_expired(target, effect)
signal xp_gained(player: Node, amount: float)
signal level_up(player: Node, new_level: int)
# Elements
signal element_damage_dealt(attacker, target, amount, element)
signal element_applied(target, element)
signal element_reaction(target, element_a, element_b, reaction_name)
signal dialog_opened(player: Node, npc: Node)
signal dialog_closed(player: Node)
signal chat_message(peer_id: int, sender_name: String, text: String)
signal scene_change_requested(scene_path: String)

View File

@@ -1 +1 @@
uid://g7a7xkg1pgb4
uid://361x7bdk2j6v

31
autoloads/game_lore.gd Normal file
View File

@@ -0,0 +1,31 @@
extends Node
const WORLD_LORE: String = """Das Land heißt Aerwen. Vor siebzig Wintern öffnete sich der erste Schlund über Aerwen. Niemand weiß, woher sie kommen. Die Alten nennen es 'das Stille Beben' — der Himmel setzte für einen Atemzug aus, dann waren die ersten Risse da.
Die Schlünde sind Tore zu einer Anderseite — einem Land, das parallel zu unserem zu liegen scheint, aber gehässiger ist. Was dort lebt, atmet durch die Tore in unsere Welt: Knochenwächter, Schattenwölfe, manchmal Schlimmeres. Die meisten Dörfer und Städte sind in den letzten siebzig Jahren gefallen. Aerwen ist heute eine Karte aus leeren Höfen, verfallenen Stadtmauern und einigen wenigen sturen Siedlungen.
Schimmerthal ist eine dieser Siedlungen. Es liegt in einer flachen Senke, umgeben von brüchigen Wegen und verlassenen Gehöften. Die Bewohner nennen sich selbst 'die Sturen' — die, die nicht weiterziehen wollten oder konnten. Im Zentrum steht die Krumme-Wagen-Taverne, benannt nach einem verkrümmten Handwagen vor der Tür, der dort steht, seit niemand mehr lebt, der sich daran erinnern kann.
Unter der Taverne liegt ein Anker — ein alter Stein, von dem niemand mehr weiß, wer ihn gesetzt hat. Solange der Anker steht, schwächt er die Schlünde im Umkreis und lässt die Reisenden nicht vollständig sterben.
Die Schlünde (was Reisende 'Portale' nennen) atmen. Alle paar Stunden öffnet sich irgendwo in der Nähe ein neuer. Wer hindurchschreitet, kommt in einer Höhle der Anderseite heraus. Tötet man die Wächter im Schlund, kollabiert der Riss. Aber er wird ersetzt — irgendwo weiter draußen reißt ein neuer, stärkerer Schlund auf. Die Anderseite gibt nicht auf.
Etwa einmal pro Atemzug (so nennen die Bewohner die Wellen) reißt der Himmel rot auf — ein 'Tor des Herrn'. Dahinter sitzt eine große Bestie, ein Herr der Anderseite, der einen Schwarm anführt. Wird der Herr besiegt, beruhigt sich die Anderseite kurz. Danach kehrt sie wieder, lauter als vorher — der nächste Atemzug bringt stärkere Wächter. Verstreicht aber die Stunde, ohne dass der Herr fällt, dann erwacht er und führt seinen Schwarm durch den Riss, direkt auf die Taverne zu. Das nennen die Bewohner 'die Invasion'. Fällt die Taverne, fällt der Anker. Niemand weiß, was dann mit dem Land geschieht. Die wenigen, die einmal eine Invasion überlebt haben, sprechen nicht darüber.
Reisende werden die genannt, die nicht ganz sterben können. Wer einmal den Anker unter der Taverne berührt hat, den lässt der Tod los: ihre Wunden schließen sich, sie tauchen am Anker wieder auf, wenn sie fallen — solange der Anker steht. Es ist kein Segen. Manche im Dorf flüstern, ein Reisender trage ein Stück Anderseite in sich. Nur Reisende können tief in die Schlünde gehen, weil die Anderseite gewöhnliche Lebende zerreißt; einen Reisenden setzt sie nur unangenehm zurück. Reisende kommen selten und gehen oft. Niemand weiß, warum der Anker manche markiert und andere nicht.
Die wichtigsten Bewohner: Brena, Wirtin der Krumme-Wagen-Taverne, geboren in Schimmerthal, übernahm das Haus von ihrer Großmutter Mara, die als Kind den ersten Schlund aufreißen sah. Halvor, der Schmied, verlor sein rechtes Auge an einen Knochenwächter im fünften Sommer nach den Schlünden. Eyrie, die Murmlerin, ist alt genug, um sich an 'die Zeit davor' zu erinnern, und liest die Risse — sie weiß oft tagelang vorher, wann ein roter Schlund aufbricht. Rolf, der Bauer, bestellt die kleinen Felder östlich des Dorfes und hat zwei Söhne an die Schlünde verloren.
Geschichten im Dorf: Manche flüstern, die Anderseite seien wir selbst, vor langer Zeit — das gilt als Ammenmärchen. Andere sagen, der Anker sei kein Stein, sondern ein Versprechen, das jemand vor sehr langer Zeit gegeben hat. Eyrie sagt selten etwas dazu; wenn man sie fragt, lächelt sie nur: 'Frag den Anker, wenn du willst.' Es gibt Reisende, die behaupten, in der Anderseite manchmal Häuser zu sehen, die genau so aussehen wie die im Dorf. Halvor schnaubt, wenn er das hört."""
const DIALECT_HINTS: String = """- Sage 'Schlund' oder 'Riss' oder 'Tor' statt 'Portal'.
- Sage 'Atemzug' statt 'Welle'.
- Sage 'Reisender' (m) / 'Reisende' (f) statt 'Spieler'.
- Sage 'Wächter' oder 'Herr' statt 'Boss'.
- Sage 'Anderseite' für das, wohin die Schlünde führen.
- Sage 'Anker' für den Stein unter der Taverne.
- Im Zweifel ehrlich: 'Das weiß niemand.' statt erfinden."""
func build_npc_context(_profile) -> String:
return "WELT:\n%s\n\nSPRACHE:\n%s" % [WORLD_LORE, DIALECT_HINTS]

View File

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

39
autoloads/game_state.gd Normal file
View File

@@ -0,0 +1,39 @@
extends Node
const ROLE_TANK: int = 0
const ROLE_DAMAGE: int = 1
const ROLE_HEALER: int = 2
const SCENE_MAIN_MENU: String = "res://scenes/menu/main_menu.tscn"
const SCENE_LOBBY: String = "res://scenes/menu/lobby.tscn"
const SCENE_WORLD: String = "res://scenes/world/world.tscn"
const SCENE_DUNGEON: String = "res://scenes/dungeon/dungeon.tscn"
const SCENE_OPTIONS: String = "res://scenes/menu/options_menu.tscn"
var current_scene: String = SCENE_MAIN_MENU
var paused: bool = false
var run_seed: int = 0
var dungeon_seed: int = 0
var dungeon_red: bool = false
var current_wave: int = 1
var portal_return_position: Vector3 = Vector3.ZERO
func reset_run() -> void:
run_seed = randi()
current_wave = 1
dungeon_seed = 0
dungeon_red = false
paused = false
Stats.clear_all()
func change_scene(path: String) -> void:
current_scene = path
EventBus.scene_change_requested.emit(path)
call_deferred("_do_change_scene", path)
func _do_change_scene(path: String) -> void:
get_tree().change_scene_to_file(path)
func set_paused(value: bool) -> void:
paused = value
get_tree().paused = value

View File

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

148
autoloads/net.gd Normal file
View File

@@ -0,0 +1,148 @@
extends Node
const DEFAULT_PORT: int = 7777
const MAX_PLAYERS: int = 4
signal peer_connected(id: int)
signal peer_disconnected(id: int)
signal connected_to_server()
signal connection_failed()
signal server_disconnected()
signal world_ready()
signal peer_world_loaded(peer_id: int)
var player_names: Dictionary = {}
var local_name: String = "Player"
var _expected_peers: Array = []
var _ready_peers: Array = []
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected_to_server)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
func host(port: int = DEFAULT_PORT, max_clients: int = MAX_PLAYERS - 1) -> bool:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(port, max_clients)
if err != OK:
push_error("Net: failed to host on port %d (err=%d)" % [port, err])
return false
multiplayer.multiplayer_peer = peer
player_names[1] = local_name
return true
func join(address: String, port: int = DEFAULT_PORT) -> bool:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_client(address, port)
if err != OK:
push_error("Net: failed to join %s:%d (err=%d)" % [address, port, err])
return false
multiplayer.multiplayer_peer = peer
return true
func host_singleplayer() -> bool:
if multiplayer.multiplayer_peer != null:
multiplayer.multiplayer_peer.close()
var peer := OfflineMultiplayerPeer.new()
multiplayer.multiplayer_peer = peer
player_names.clear()
player_names[1] = local_name
return true
func disconnect_net() -> void:
if multiplayer.multiplayer_peer != null:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
player_names.clear()
func is_host() -> bool:
return multiplayer.multiplayer_peer == null or multiplayer.is_server()
func is_authority_for(node: Node) -> bool:
return node.is_multiplayer_authority()
func local_id() -> int:
if multiplayer.multiplayer_peer == null:
return 1
return multiplayer.get_unique_id()
func _on_peer_connected(id: int) -> void:
peer_connected.emit(id)
if is_host():
_register_name.rpc_id(id, 1, local_name)
func _on_peer_disconnected(id: int) -> void:
player_names.erase(id)
peer_disconnected.emit(id)
func _on_connected_to_server() -> void:
player_names[1] = "Host"
player_names[multiplayer.get_unique_id()] = local_name
_register_name.rpc(multiplayer.get_unique_id(), local_name)
connected_to_server.emit()
func _on_connection_failed() -> void:
multiplayer.multiplayer_peer = null
connection_failed.emit()
func _on_server_disconnected() -> void:
multiplayer.multiplayer_peer = null
player_names.clear()
server_disconnected.emit()
@rpc("any_peer", "reliable", "call_local")
func _register_name(peer_id: int, name: String) -> void:
player_names[peer_id] = name
func mark_world_loaded() -> void:
if multiplayer.multiplayer_peer == null:
_expected_peers = [1]
_ready_peers = [1]
world_ready.emit()
return
if is_host():
_expected_peers = [1]
for p in multiplayer.get_peers():
_expected_peers.append(p)
_ready_peers = [1]
if _all_ready():
_broadcast_ready.rpc()
else:
_client_world_loaded.rpc_id(1, multiplayer.get_unique_id())
@rpc("any_peer", "reliable")
func _client_world_loaded(peer_id: int) -> void:
if not is_host():
return
if not peer_id in _expected_peers:
_expected_peers.append(peer_id)
if not peer_id in _ready_peers:
_ready_peers.append(peer_id)
peer_world_loaded.emit(peer_id)
if _all_ready():
_broadcast_ready.rpc()
@rpc("authority", "reliable")
func _load_scene_for_peer(path: String) -> void:
GameState.change_scene(path)
func tell_peer_to_load_scene(peer_id: int, path: String) -> void:
if not is_host():
return
_load_scene_for_peer.rpc_id(peer_id, path)
@rpc("authority", "reliable", "call_local")
func _broadcast_ready() -> void:
world_ready.emit()
func _all_ready() -> bool:
for p in _expected_peers:
if not p in _ready_peers:
return false
return true
func reset_world_ready() -> void:
_expected_peers.clear()
_ready_peers.clear()

1
autoloads/net.gd.uid Normal file
View File

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

View File

@@ -1,145 +0,0 @@
extends Node
enum Role { TANK, DAMAGE, HEALER }
# Basis (aus Resource geladen)
var base: PlayerStats
var speed := 5.0
var jump_velocity := 4.5
var target_range := 20.0
var combat_timeout := 3.0
var respawn_time := 3.0
var gcd_time := 0.5
var aa_cooldown := 0.5
# Laufzeit
var health := 100.0
var max_health := 100.0
var health_regen := 0.0
var shield := 0.0
var max_shield := 0.0
var shield_regen_delay := 3.0
var shield_regen_time := 5.0
var shield_regen_timer := 0.0
var alive := true
# Buffs
var buff_damage := 1.0
var buff_heal := 1.0
var buff_shield := 1.0
# Rolle
var current_role: int = Role.DAMAGE
var ability_set: AbilitySet = null
# Kampf
var target: Node3D = null
var in_combat := false
var combat_timer := 0.0
# Cooldowns
var cooldowns: Array[float] = []
var max_cooldowns: Array[float] = []
var gcd := 0.0
var aa_timer := 0.0
# Szenenwechsel
var portal_position := Vector3.ZERO
var returning_from_dungeon := false
var dungeon_cleared := false
# Cache für Szenenwechsel
var _cache: Dictionary = {}
func init_from_resource(res: PlayerStats) -> void:
base = res
speed = res.speed
jump_velocity = res.jump_velocity
target_range = res.target_range
combat_timeout = res.combat_timeout
respawn_time = res.respawn_time
gcd_time = res.gcd_time
aa_cooldown = res.aa_cooldown
if _cache.is_empty():
health = res.max_health
max_health = res.max_health
health_regen = res.health_regen
shield = res.max_shield
max_shield = res.max_shield
shield_regen_delay = res.shield_regen_delay
shield_regen_time = res.shield_regen_time
shield_regen_timer = 0.0
alive = true
buff_damage = 1.0
buff_heal = 1.0
buff_shield = 1.0
else:
_restore_cache()
cooldowns.resize(5)
cooldowns.fill(0.0)
max_cooldowns.resize(5)
max_cooldowns.fill(0.0)
gcd = 0.0
aa_timer = 0.0
func set_health(value: float) -> void:
health = value
EventBus.health_changed.emit(self, health, max_health)
if health <= 0 and alive:
alive = false
EventBus.entity_died.emit(self)
func set_shield(value: float) -> void:
shield = value
EventBus.shield_changed.emit(self, shield, max_shield)
func set_role(role: int) -> void:
current_role = role
EventBus.role_changed.emit(self, current_role)
func set_target(new_target: Node3D) -> void:
target = new_target
EventBus.target_changed.emit(self, target)
func respawn() -> void:
health = max_health
shield = max_shield
alive = true
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
EventBus.player_respawned.emit(self)
func save_cache() -> void:
_cache = {
"health": health,
"max_health": max_health,
"health_regen": health_regen,
"shield": shield,
"max_shield": max_shield,
"shield_regen_delay": shield_regen_delay,
"shield_regen_time": shield_regen_time,
"alive": alive,
"buff_damage": buff_damage,
"buff_heal": buff_heal,
"buff_shield": buff_shield,
}
func clear_cache() -> void:
_cache.clear()
portal_position = Vector3.ZERO
returning_from_dungeon = false
dungeon_cleared = false
func _restore_cache() -> void:
health = _cache.get("health", max_health)
max_health = _cache.get("max_health", max_health)
health_regen = _cache.get("health_regen", 0.0)
shield = _cache.get("shield", 0.0)
max_shield = _cache.get("max_shield", 0.0)
shield_regen_delay = _cache.get("shield_regen_delay", 3.0)
shield_regen_time = _cache.get("shield_regen_time", 5.0)
alive = _cache.get("alive", true)
buff_damage = _cache.get("buff_damage", 1.0)
buff_heal = _cache.get("buff_heal", 1.0)
buff_shield = _cache.get("buff_shield", 1.0)
_cache.clear()

View File

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

View File

@@ -1,45 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: PortalStats) -> void:
var thresholds: Array[float] = base.thresholds.duplicate()
var triggered: Array[bool] = []
triggered.resize(thresholds.size())
triggered.fill(false)
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"alive": true,
"spawn_count": base.spawn_count,
"thresholds": thresholds,
"triggered": triggered,
}
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)

View File

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

46
autoloads/save_load.gd Normal file
View File

@@ -0,0 +1,46 @@
extends Node
const SAVE_DIR: String = "user://saves/"
func ensure_dir() -> void:
DirAccess.make_dir_recursive_absolute(SAVE_DIR)
func save_path(slot: String) -> String:
return SAVE_DIR + slot + ".save"
func save_run(slot: String, payload: Dictionary) -> bool:
ensure_dir()
var f := FileAccess.open(save_path(slot), FileAccess.WRITE)
if f == null:
push_error("SaveLoad: cannot open %s for write" % slot)
return false
f.store_string(JSON.stringify(payload, " "))
f.close()
return true
func load_run(slot: String) -> Dictionary:
var path := save_path(slot)
if not FileAccess.file_exists(path):
return {}
var f := FileAccess.open(path, FileAccess.READ)
var text := f.get_as_text()
f.close()
var data: Variant = JSON.parse_string(text)
if typeof(data) != TYPE_DICTIONARY:
return {}
return data
func list_slots() -> Array[String]:
ensure_dir()
var out: Array[String] = []
var d := DirAccess.open(SAVE_DIR)
if d == null:
return out
d.list_dir_begin()
var fname := d.get_next()
while fname != "":
if not d.current_is_dir() and fname.ends_with(".save"):
out.append(fname.trim_suffix(".save"))
fname = d.get_next()
d.list_dir_end()
return out

View File

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

124
autoloads/stats.gd Normal file
View File

@@ -0,0 +1,124 @@
extends Node
const SYNCED_STATS: Array = [
"health", "shield", "max_health", "max_shield",
"role", "level", "xp", "xp_to_next",
"buff_damage", "buff_heal", "buff_shield",
]
const SYNC_INTERVAL: float = 0.10
var _entities: Dictionary = {}
var _player_cache: Dictionary = {}
var _dirty: Dictionary = {}
var _accum: float = 0.0
func _ready() -> void:
set_process(true)
func _process(delta: float) -> void:
_accum += delta
if _accum < SYNC_INTERVAL:
return
_accum = 0.0
if multiplayer.multiplayer_peer == null or multiplayer.multiplayer_peer is OfflineMultiplayerPeer or not multiplayer.is_server():
_dirty.clear()
return
if _dirty.is_empty():
return
for entity in _dirty.keys():
if not is_instance_valid(entity) or not entity.is_inside_tree():
continue
_sync_stats_batch.rpc(String(entity.get_path()), _dirty[entity])
_dirty.clear()
@rpc("authority", "unreliable_ordered")
func _sync_stats_batch(path_str: String, changes: Dictionary) -> void:
var entity := get_node_or_null(NodePath(path_str))
if entity == null or not entity in _entities:
return
for stat in changes.keys():
_entities[entity][stat] = changes[stat]
match stat:
"health":
EventBus.health_changed.emit(entity, changes[stat], _entities[entity].get("max_health", 100.0))
"shield":
EventBus.shield_changed.emit(entity, changes[stat], _entities[entity].get("max_shield", 0.0))
"role":
EventBus.role_changed.emit(entity, changes[stat])
"level":
EventBus.level_up.emit(entity, changes[stat])
"buff_damage", "buff_heal", "buff_shield":
EventBus.buff_changed.emit(entity, StringName(stat), changes[stat])
func register(entity: Node, base_resource: Resource) -> void:
if not is_instance_valid(entity):
return
var data: Dictionary = {}
for prop in base_resource.get_property_list():
var name: String = prop.name
if not (prop.usage & PROPERTY_USAGE_STORAGE):
continue
if name in ["resource_local_to_scene", "resource_path", "resource_name", "resource_scene_unique_id", "script"]:
continue
data[name] = base_resource.get(name)
data["health"] = data.get("max_health", 100.0)
data["shield"] = data.get("max_shield", 0.0)
data["shield_regen_timer"] = 0.0
data["base_resource"] = base_resource
_entities[entity] = data
EventBus.entity_registered.emit(entity)
func deregister(entity: Node) -> void:
if entity in _entities:
_entities.erase(entity)
EventBus.entity_deregistered.emit(entity)
func has(entity: Node) -> bool:
return entity in _entities
func get_stat(entity: Node, stat: String, default: Variant = 0.0) -> Variant:
if entity in _entities:
return _entities[entity].get(stat, default)
return default
func set_stat(entity: Node, stat: String, value: Variant) -> void:
if not entity in _entities:
return
var prev: Variant = _entities[entity].get(stat)
_entities[entity][stat] = value
if stat in SYNCED_STATS and prev != value and multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.is_server() and is_instance_valid(entity) and entity.is_inside_tree():
if not entity in _dirty:
_dirty[entity] = {}
_dirty[entity][stat] = value
func get_all(entity: Node) -> Dictionary:
return _entities.get(entity, {})
func entities() -> Array:
return _entities.keys()
func entities_in_group(group: StringName) -> Array:
var out: Array = []
for e in _entities.keys():
if is_instance_valid(e) and e.is_in_group(group):
out.append(e)
return out
func cache_player(peer_id: int, entity: Node) -> void:
if entity in _entities:
_player_cache[peer_id] = _entities[entity].duplicate(true)
func restore_player(peer_id: int, entity: Node) -> void:
if peer_id in _player_cache:
_entities[entity] = _player_cache[peer_id].duplicate(true)
func clear_player_cache(peer_id: int = -1) -> void:
if peer_id == -1:
_player_cache.clear()
else:
_player_cache.erase(peer_id)
func clear_all() -> void:
_entities.clear()
_player_cache.clear()

1
autoloads/stats.gd.uid Normal file
View File

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

View File

@@ -1,8 +0,0 @@
extends Resource
class_name BaseStats
@export var max_health := 100.0
@export var health_regen := 0.0
@export var max_shield := 0.0
@export var shield_regen_delay := 3.0
@export var shield_regen_time := 5.0

View File

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

46
export_presets.cfg Normal file
View File

@@ -0,0 +1,46 @@
[preset.0]
name="Linux"
platform="Linux"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=""
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
export DISPLAY=:0
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
rm -rf \"{temp_dir}\""

View File

@@ -11,17 +11,18 @@ config_version=5
[application]
config/name="mmo"
run/main_scene="res://scenes/world/world.tscn"
run/main_scene="res://scenes/menu/main_menu.tscn"
config/features=PackedStringArray("4.6", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
EventBus="*res://autoloads/event_bus.gd"
PlayerData="*res://autoloads/player_stats.gd"
EnemyData="*res://autoloads/enemy_stats.gd"
BossData="*res://autoloads/boss_stats.gd"
PortalData="*res://autoloads/portal_stats.gd"
Net="*res://autoloads/net.gd"
Stats="*res://autoloads/stats.gd"
GameState="*res://autoloads/game_state.gd"
SaveLoad="*res://autoloads/save_load.gd"
GameLore="*res://autoloads/game_lore.gd"
[dotnet]
@@ -49,51 +50,109 @@ move_right={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
jump={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
ability_1={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null)
]
}
ability_2={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null)
]
}
ability_3={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null)
]
}
ability_4={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":52,"location":0,"echo":false,"script":null)
]
}
ability_5={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":53,"key_label":0,"unicode":53,"location":0,"echo":false,"script":null)
]
}
target_next={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
class_tank={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null)
]
}
class_damage={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null)
]
}
class_healer={
"deadzone": 0.2,
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null)
]
}
interact={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
]
}
inventory={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null)
]
}
crafting={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
]
}
build_mode={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null)
]
}
chat={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":121,"location":0,"echo":false,"script":null)
]
}
map={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":109,"location":0,"echo":false,"script":null)
]
}
pause={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
rotate_build={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
]
}
click_select={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
camera_drag={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
[layer_names]
3d_physics/layer_1="World"
3d_physics/layer_2="Player"
3d_physics/layer_3="Enemy"
3d_physics/layer_4="Hitbox"
3d_physics/layer_5="Building"
[physics]

View File

@@ -0,0 +1,20 @@
class_name Ability
extends Resource
enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var ability_name: StringName = &""
@export var type: Type = Type.SINGLE
@export var damage: float = 0.0
@export var ability_range: float = 5.0
@export var cooldown: float = 2.0
@export var uses_gcd: bool = true
@export var aoe_radius: float = 0.0
@export var is_heal: bool = false
@export var shield_value: float = 0.0
@export var shield_multiplier: float = 0.0
@export var passive_stat: StringName = &""
@export var passive_value: float = 0.5
@export var passive_radius: float = 50.0
@export var element: int = Element.NONE
@export var icon: Texture2D

View File

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

View File

@@ -1,7 +1,9 @@
extends Resource
class_name AbilitySet
extends Resource
@export var abilities: Array[Ability] = []
@export var aa_damage: float = 10.0
@export var role_name: StringName = &""
@export var aa_damage: float = 5.0
@export var aa_range: float = 10.0
@export var aa_is_heal: bool = false
@export var abilities: Array[Ability] = []
@export var color: Color = Color.WHITE

View File

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

View File

@@ -0,0 +1,13 @@
class_name Building
extends Resource
enum Type { FLOOR, WALL, DOOR, ROOF }
@export var building_id: StringName = &""
@export var display_name: String = ""
@export var type: Type = Type.WALL
@export var size: Vector3 = Vector3(1, 1, 1)
@export var color: Color = Color.GRAY
@export var icon: Texture2D
@export var material_item: Item
@export var material_cost: int = 1

View File

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

View File

@@ -0,0 +1,15 @@
class_name Effect
extends Resource
enum Type { BUFF, DEBUFF, AURA, DOT, HOT }
@export var effect_name: StringName = &""
@export var type: Type = Type.BUFF
@export var stat: StringName = &""
@export var value: float = 0.0
@export var is_multiplier: bool = false
@export var duration: float = -1.0
@export var aura_radius: float = 0.0
@export var tick_interval: float = 0.0
@export var element: int = Element.NONE
@export var icon: Texture2D

View File

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

View File

@@ -0,0 +1,19 @@
class_name Element
extends RefCounted
const NONE: int = 0
const FIRE: int = 1
static func name_of(e: int) -> String:
match e:
FIRE:
return "Fire"
_:
return "None"
static func color_of(e: int) -> Color:
match e:
FIRE:
return Color(1.0, 0.4, 0.1)
_:
return Color.WHITE

View File

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

12
resources/items/item.gd Normal file
View File

@@ -0,0 +1,12 @@
class_name Item
extends Resource
enum Category { ESSENCE, MATERIAL, EQUIPMENT, CONSUMABLE }
@export var item_id: StringName = &""
@export var display_name: String = ""
@export var category: Category = Category.MATERIAL
@export var max_stack: int = 99
@export var icon: Texture2D
@export var equip_slot: StringName = &""
@export var stat_bonus: Dictionary = {}

View File

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

View File

@@ -0,0 +1,9 @@
class_name Recipe
extends Resource
@export var recipe_id: StringName = &""
@export var display_name: String = ""
@export var output_item: Item
@export var output_count: int = 1
@export var inputs: Array[Item] = []
@export var input_counts: Array[int] = []

View File

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

View File

@@ -0,0 +1,10 @@
class_name NpcProfile
extends Resource
@export var npc_id: StringName = &""
@export var display_name: String = ""
@export_multiline var lore: String = ""
@export_multiline var personality: String = ""
@export_multiline var fallback_text: String = "..."
@export var color: Color = Color(0.8, 0.7, 0.5)
@export var greeting: String = "Hallo Reisender."

View File

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

View File

@@ -0,0 +1,8 @@
class_name BaseStats
extends Resource
@export var max_health: float = 100.0
@export var health_regen: float = 0.0
@export var max_shield: float = 0.0
@export var shield_regen: float = 0.0
@export var shield_regen_delay: float = 5.0

View File

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

View File

@@ -1,2 +1,4 @@
extends EnemyStats
class_name BossStats
extends EnemyStats
@export var enrage_time: float = 60.0

View File

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

View File

@@ -0,0 +1,2 @@
class_name BuildingStats
extends BaseStats

View File

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

View File

@@ -0,0 +1,11 @@
class_name EnemyStats
extends BaseStats
@export var speed: float = 3.0
@export var attack_range: float = 2.0
@export var attack_damage: float = 5.0
@export var attack_cooldown: float = 1.5
@export var aggro_radius: float = 12.0
@export var leash_radius: float = 25.0
@export var xp_value: float = 5.0
@export var loot_chance: float = 1.0

View File

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

View File

@@ -0,0 +1,7 @@
class_name GateStats
extends BaseStats
@export var spawn_count: int = 5
@export var spawn_interval: float = 4.0
@export var aggro_radius: float = 15.0
@export var is_red: bool = false

View File

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

View File

@@ -0,0 +1,18 @@
class_name PlayerStats
extends BaseStats
@export var speed: float = 5.0
@export var jump_velocity: float = 4.5
@export var target_range: float = 20.0
@export var combat_timeout: float = 5.0
@export var respawn_time: float = 3.0
@export var gcd_time: float = 0.5
@export var aa_cooldown: float = 0.5
@export var level: int = 1
@export var xp: float = 0.0
@export var xp_to_next: float = 50.0
@export var buff_damage: float = 1.0
@export var buff_heal: float = 1.0
@export var buff_shield: float = 1.0

View File

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

View File

@@ -0,0 +1,5 @@
class_name PortalStats
extends BaseStats
@export var lifetime: float = 0.0
@export var is_red: bool = false

View File

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

View File

@@ -0,0 +1,2 @@
class_name VillageStats
extends BaseStats

View File

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

View File

@@ -1,254 +1,133 @@
[gd_scene format=3]
[gd_scene load_steps=23 format=3 uid="uid://b0dungeon0001"]
[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="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"]
[ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"]
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"]
[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"]
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"]
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"]
[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"]
[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"]
[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"]
[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"]
[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"]
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"]
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"]
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"]
[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://b0player00001" path="res://scenes/entities/player/player.tscn" id="2"]
[ext_resource type="Script" path="res://systems/health_system.gd" id="4"]
[ext_resource type="Script" path="res://systems/shield_system.gd" id="5"]
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="6"]
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="7"]
[ext_resource type="Script" path="res://systems/role_system.gd" id="8"]
[ext_resource type="Script" path="res://systems/effect_system.gd" id="9"]
[ext_resource type="Script" path="res://systems/element_system.gd" id="10"]
[ext_resource type="Script" path="res://systems/aggro_system.gd" id="11"]
[ext_resource type="Script" path="res://systems/combat/ability_system.gd" id="12"]
[ext_resource type="Script" path="res://systems/combat/auto_attack_system.gd" id="13"]
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="14"]
[ext_resource type="Script" path="res://systems/xp_system.gd" id="17"]
[ext_resource type="Script" path="res://systems/loot_system.gd" id="18"]
[ext_resource type="Script" path="res://systems/inventory_system.gd" id="19"]
[ext_resource type="Script" path="res://systems/chat_system.gd" id="24"]
[ext_resource type="Script" path="res://systems/map_system.gd" id="25"]
[ext_resource type="PackedScene" uid="uid://b0hud00001" path="res://scenes/hud/hud.tscn" id="26"]
[ext_resource type="Script" path="res://systems/audio_system.gd" id="27"]
[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="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"]
sky_top_color = Color(0.1, 0.05, 0.1, 1)
sky_horizon_color = Color(0.15, 0.05, 0.05, 1)
ground_horizon_color = Color(0.1, 0.05, 0.05, 1)
ground_bottom_color = Color(0.0, 0.0, 0.0, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
albedo_color = Color(0.2, 0.18, 0.15, 1)
[sub_resource type="Sky" id="Sky_1"]
sky_material = SubResource("ProceduralSkyMaterial_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)
[sub_resource type="Environment" id="Environment_1"]
background_mode = 2
sky = SubResource("Sky_1")
ambient_light_source = 3
ambient_light_color = Color(0.2, 0.18, 0.2, 1)
ambient_light_energy = 0.4
[node name="Dungeon" type="Node3D"]
script = ExtResource("1")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_1")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 30, 0)
light_energy = 0.7
[node name="DungeonGeometry" type="Node3D" parent="."]
[node name="Systems" type="Node" parent="."]
[node name="HealthSystem" type="Node" parent="Systems"]
script = ExtResource("health_system")
[node name="DamageSystem" type="Node" parent="Systems"]
script = ExtResource("damage_system")
[node name="HealSystem" type="Node" parent="Systems"]
script = ExtResource("heal_system")
script = ExtResource("4")
[node name="ShieldSystem" type="Node" parent="Systems"]
script = ExtResource("shield_system")
[node name="RoleSystem" type="Node" parent="Systems"]
script = ExtResource("role_system")
tank_set = ExtResource("tank_set")
damage_set = ExtResource("damage_set")
healer_set = ExtResource("healer_set")
[node name="AbilitySystem" type="Node" parent="Systems"]
script = ExtResource("ability_system")
[node name="AttackSystem" type="Node" parent="Systems"]
script = ExtResource("attack_system")
[node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("cooldown_system")
[node name="TargetingSystem" type="Node" parent="Systems"]
script = ExtResource("targeting_system")
[node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("aggro_system")
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_tracker")
[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_decay")
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_events")
[node name="AISystem" type="Node" parent="Systems"]
script = ExtResource("ai_system")
script = ExtResource("5")
[node name="RespawnSystem" type="Node" parent="Systems"]
script = ExtResource("respawn_system")
script = ExtResource("6")
[node name="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("spawn_system")
[node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("7")
[node name="AuraSystem" type="Node" parent="Systems"]
script = ExtResource("aura_system")
[node name="RoleSystem" type="Node" parent="Systems"]
script = ExtResource("8")
[node name="BuffSystem" type="Node" parent="Systems"]
script = ExtResource("buff_system")
[node name="DebuffSystem" type="Node" parent="Systems"]
script = ExtResource("debuff_system")
[node name="EffectSystem" type="Node" parent="Systems"]
script = ExtResource("9")
[node name="ElementSystem" type="Node" parent="Systems"]
script = ExtResource("element_system")
script = ExtResource("10")
[node name="HudSystem" type="Node" parent="Systems"]
script = ExtResource("hud_system")
[node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("11")
[node name="NameplateSystem" type="Node" parent="Systems"]
script = ExtResource("nameplate_system")
[node name="AbilitySystem" type="Node" parent="Systems"]
script = ExtResource("12")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_1")
[node name="AutoAttackSystem" type="Node" parent="Systems"]
script = ExtResource("13")
[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="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("14")
[node name="BodenCollision" type="StaticBody3D" parent="."]
[node name="LootSystem" type="Node" parent="Systems"]
script = ExtResource("18")
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
shape = SubResource("WorldBoundaryShape3D_1")
[node name="XpSystem" type="Node" parent="Systems"]
script = ExtResource("17")
[node name="WallSouth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -5.25)
[node name="InventorySystem" type="Node" parent="Systems"]
script = ExtResource("19")
[node name="Mesh" type="MeshInstance3D" parent="WallSouth"]
mesh = SubResource("BoxMesh_north_south")
[node name="ChatSystem" type="Node" parent="Systems"]
script = ExtResource("24")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallSouth"]
shape = SubResource("BoxShape3D_north_south")
[node name="MapSystem" type="Node" parent="Systems"]
script = ExtResource("25")
[node name="WallNorth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 85.25)
[node name="AudioSystem" type="Node" parent="Systems"]
script = ExtResource("27")
[node name="Mesh" type="MeshInstance3D" parent="WallNorth"]
mesh = SubResource("BoxMesh_north_south")
[node name="EntityRoot" type="Node3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallNorth"]
shape = SubResource("BoxShape3D_north_south")
[node name="Players" type="Node3D" parent="EntityRoot"]
[node name="WallEast" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.75, 1.5, 40)
[node name="Enemies" type="Node3D" parent="EntityRoot"]
[node name="Mesh" type="MeshInstance3D" parent="WallEast"]
mesh = SubResource("BoxMesh_east_west")
[node name="Loot" type="Node3D" parent="EntityRoot"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallEast"]
shape = SubResource("BoxShape3D_east_west")
[node name="Portals" type="Node3D" parent="EntityRoot"]
[node name="WallWest" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.75, 1.5, 40)
[node name="Buildings" type="Node3D" parent="EntityRoot"]
[node name="Mesh" type="MeshInstance3D" parent="WallWest"]
mesh = SubResource("BoxMesh_east_west")
[node name="Gates" type="Node3D" parent="EntityRoot"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallWest"]
shape = SubResource("BoxShape3D_east_west")
[node name="Npcs" type="Node3D" parent="EntityRoot"]
[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="PlayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/player/player.tscn")
spawn_path = NodePath("../EntityRoot/Players")
[node name="Player" parent="." instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -3)
[node name="EnemySpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/enemy/enemy.tscn")
spawn_path = NodePath("../EntityRoot/Enemies")
[node name="HUD" parent="." instance=ExtResource("hud")]
[node name="LootSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/loot/loot_drop.tscn")
spawn_path = NodePath("../EntityRoot/Loot")
[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="." groups=["boss"] instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75)
stats = ExtResource("boss_stats")
[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/world.tscn"
is_exit = true
[node name="DungeonSystem" type="Node" parent="Systems"]
script = ExtResource("dungeon_system")
[node name="HUD" parent="." instance=ExtResource("26")]

View File

@@ -0,0 +1,132 @@
extends Node
const ROOM_HEIGHT: float = 4.0
const WALL_THICKNESS: float = 0.4
const CORRIDOR_WIDTH: float = 4.0
const DOOR_WIDTH: float = 4.5
var rng: RandomNumberGenerator
var rooms: Array = []
func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dictionary:
rng = RandomNumberGenerator.new()
rng.seed = seed
rooms.clear()
var room_count: int = rng.randi_range(5, 8)
var pos := Vector3(0, 0, 0)
var dir := Vector3(0, 0, -1)
for i in range(room_count):
var w: float = rng.randf_range(8.0, 14.0)
var d: float = rng.randf_range(8.0, 14.0)
rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d), "openings": []})
if i == room_count - 1:
break
var corridor_len: float = rng.randf_range(4.0, 8.0)
var step: Vector3 = dir * (max(w, d) * 0.5 + corridor_len + 4.0)
pos += step
if rng.randf() < 0.5:
var rotate_left: bool = rng.randf() < 0.5
dir = dir.rotated(Vector3.UP, PI * 0.5 * (1 if rotate_left else -1))
_compute_openings()
_build_geometry(parent)
return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos}
func _compute_openings() -> void:
for i in range(rooms.size() - 1):
var rel: Vector3 = rooms[i + 1].pos - rooms[i].pos
if abs(rel.x) > abs(rel.z):
if rel.x > 0:
rooms[i].openings.append("east")
rooms[i + 1].openings.append("west")
else:
rooms[i].openings.append("west")
rooms[i + 1].openings.append("east")
else:
if rel.z > 0:
rooms[i].openings.append("south")
rooms[i + 1].openings.append("north")
else:
rooms[i].openings.append("north")
rooms[i + 1].openings.append("south")
func _build_geometry(parent: Node3D) -> void:
for r in rooms:
_build_room(parent, r.pos, r.size, r.openings)
for i in range(rooms.size() - 1):
_build_corridor(parent, rooms[i].pos, rooms[i + 1].pos)
func _build_room(parent: Node3D, center: Vector3, size: Vector3, openings: Array) -> void:
_add_floor(parent, center, Vector2(size.x, size.z))
var hw: float = size.x * 0.5
var hd: float = size.z * 0.5
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "north", openings, true)
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "south", openings, true)
_add_wall_with_opening(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "west", openings, false)
_add_wall_with_opening(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "east", openings, false)
func _add_wall_with_opening(parent: Node3D, center: Vector3, size: Vector3, side: String, openings: Array, axis_x: bool) -> void:
if not openings.has(side):
_add_wall(parent, center, size)
return
if axis_x:
var seg_len: float = (size.x - DOOR_WIDTH) * 0.5
if seg_len <= 0.1:
return
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
_add_wall(parent, center + Vector3(-seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
_add_wall(parent, center + Vector3(seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
else:
var seg_len: float = (size.z - DOOR_WIDTH) * 0.5
if seg_len <= 0.1:
return
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
_add_wall(parent, center + Vector3(0, 0, -seg_offset), Vector3(size.x, size.y, seg_len))
_add_wall(parent, center + Vector3(0, 0, seg_offset), Vector3(size.x, size.y, seg_len))
func _build_corridor(parent: Node3D, from: Vector3, to: Vector3) -> void:
var mid: Vector3 = (from + to) * 0.5
var d: float = from.distance_to(to)
var dir: Vector3 = (to - from).normalized()
_add_floor(parent, Vector3(mid.x, 0, mid.z), Vector2(d, CORRIDOR_WIDTH) if abs(dir.x) > abs(dir.z) else Vector2(CORRIDOR_WIDTH, d))
func _add_floor(parent: Node3D, center: Vector3, size: Vector2) -> void:
var body := StaticBody3D.new()
body.collision_layer = 1
body.collision_mask = 0
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(size.x, 0.4, size.y)
mesh.mesh = box
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.25, 0.22, 0.2)
mesh.material_override = mat
mesh.position = Vector3(0, -0.2, 0)
var col := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = Vector3(size.x, 0.4, size.y)
col.shape = shape
col.position = Vector3(0, -0.2, 0)
body.add_child(mesh)
body.add_child(col)
body.position = center
parent.add_child(body)
func _add_wall(parent: Node3D, center: Vector3, size: Vector3) -> void:
var body := StaticBody3D.new()
body.collision_layer = 1
body.collision_mask = 0
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = size
mesh.mesh = box
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.35, 0.32, 0.28)
mesh.material_override = mat
var col := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = size
col.shape = shape
body.add_child(mesh)
body.add_child(col)
body.position = center
parent.add_child(body)

Some files were not shown because too many files have changed in this diff Show More