Compare commits

...

4 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
259 changed files with 5388 additions and 4529 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)

View File

@@ -1,82 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void:
var max_hp: float = base.max_health * scale
var max_sh: float = base.max_shield * scale
entities[entity] = {
"base": base,
"scale": scale,
"health": max_hp,
"max_health": max_hp,
"health_regen": base.health_regen * scale,
"shield": max_sh,
"max_shield": max_sh,
"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 apply_scale(entity: Node, scale: float) -> void:
if entity not in entities:
return
var data: Dictionary = entities[entity]
var base: EnemyStats = data["base"]
data["scale"] = scale
data["max_health"] = base.max_health * scale
data["health"] = data["max_health"]
data["health_regen"] = base.health_regen * scale
data["max_shield"] = base.max_shield * scale
data["shield"] = data["max_shield"]
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
if base.max_shield > 0:
EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"])
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,82 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void:
var max_hp: float = base.max_health * scale
var max_sh: float = base.max_shield * scale
entities[entity] = {
"base": base,
"scale": scale,
"health": max_hp,
"max_health": max_hp,
"health_regen": base.health_regen * scale,
"shield": max_sh,
"max_shield": max_sh,
"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 apply_scale(entity: Node, scale: float) -> void:
if entity not in entities:
return
var data: Dictionary = entities[entity]
var base: EnemyStats = data["base"]
data["scale"] = scale
data["max_health"] = base.max_health * scale
data["health"] = data["max_health"]
data["health_regen"] = base.health_regen * scale
data["max_shield"] = base.max_shield * scale
data["shield"] = data["max_shield"]
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
if base.max_shield > 0:
EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"])
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,73 +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()
# Effects
signal effect_requested(target, effect, source)
signal effect_applied(target, effect)
signal effect_expired(target, effect)
# Elements
signal element_damage_dealt(attacker, target, amount, element)
signal element_applied(target, element)
signal element_reaction(target, element_a, element_b, reaction_name)
# Wave
signal run_started(wave_number)
signal wave_started(wave_number)
signal wave_timer_tick(seconds_remaining)
signal wave_ended(wave_number, success)
# XP / Level
signal xp_gained(player, amount)
signal level_up(player, new_level)
# Taverne
signal tavern_damaged(current, max_val)
signal tavern_destroyed()
# Invasion
signal invasion_started(enemies)
signal invasion_ended(success)
# Game-Over
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)
signal xp_gained(player: Node, amount: float)
signal level_up(player: Node, new_level: int)
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

View File

@@ -1,19 +1,39 @@
extends Node
# Run-Zustand
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 wave_timer_remaining: float = 0.0
var run_initialized: bool = false
var portal_return_position: Vector3 = Vector3.ZERO
# Dungeon-Kontext (für XP-Zuordnung nach Clear)
var last_dungeon_variant: int = 0
# Flag für Forced Return (Timer läuft ab während Spieler im Dungeon)
var force_return_to_world: bool = false
func reset() -> void:
func reset_run() -> void:
run_seed = randi()
current_wave = 1
wave_timer_remaining = 0.0
run_initialized = false
last_dungeon_variant = 0
force_return_to_world = false
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

@@ -1 +1 @@
uid://c3jq4raqs0onf
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,195 +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
# Level / XP
const XP_PER_LEVEL: int = 50
var level: int = 1
var xp: int = 0
var xp_to_next: int = XP_PER_LEVEL
var level_scale: float = 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():
max_health = res.max_health * level_scale
health = max_health
health_regen = res.health_regen * level_scale
max_shield = res.max_shield * level_scale
shield = 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 reset_run() -> void:
clear_cache()
level = 1
xp = 0
xp_to_next = XP_PER_LEVEL
level_scale = 1.0
func add_xp(amount: int) -> void:
xp += amount
EventBus.xp_gained.emit(self, amount)
while xp >= xp_to_next:
xp -= xp_to_next
level_up()
func level_up() -> void:
level += 1
level_scale = float(_fibonacci(level))
xp_to_next = XP_PER_LEVEL * _fibonacci(level)
if base:
max_health = base.max_health * level_scale
max_shield = base.max_shield * level_scale
else:
max_health = 100.0 * level_scale
max_shield = 50.0 * level_scale
health = max_health
shield = max_shield
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
EventBus.level_up.emit(self, level)
func _fibonacci(n: int) -> int:
if n <= 1:
return 1
if n == 2:
return 2
var a := 1
var b := 2
for i in range(3, n + 1):
var c := a + b
a = b
b = c
return b
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

View File

@@ -1,38 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: Resource) -> void:
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"alive": true,
}
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.tavern_damaged.emit(value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.tavern_destroyed.emit()

View File

@@ -1 +0,0 @@
uid://822h8c1pur1a

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}\""

58
plan.md
View File

@@ -22,12 +22,12 @@ scenes/ — Darstellung + Input
healthbar_status.gd — Target-Border + Aggro-Farbwechsel
healthbar_effects.gd — Effekt-Icons auf Healthbar
player/ — Spieler + player_stats
role/ — Rollenwechsel + Ability/AbilitySet-Klassen
damage/ — set.tres + abilities/
tank/ — set.tres + abilities/
healer/ — set.tres + abilities/
role/ — Rollenwechsel + Ability/AbilitySet-Klassen
damage/ — set.tres + abilities/
tank/ — set.tres + abilities/
healer/ — set.tres + abilities/
enemy/ — Gegner + enemy_stats
boss/ — Boss + boss_stats
boss/ — Boss + boss_stats
portal/ — Portal + Gate + portal_stats
dungeon/ — Dungeon + dungeon_manager
hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects)
@@ -49,7 +49,7 @@ autoloads/ — Globaler Zustand
- Taverne
- Player
- Portale (dynamisch)
- Gegner
- Gegner
- HUD
## Architektur
@@ -254,14 +254,14 @@ autoloads/ — Globaler Zustand
- world.tscn — Hauptszene (100x100m)
- Systems (alle Systeme als Child-Nodes, siehe Systeme-Sektion)
- NavigationRegion3D
- Boden (MeshInstance3D, 100x100m PlaneMesh)
- Boden (MeshInstance3D, 100x100m PlaneMesh)
- Kollision (StaticBody3D, WorldBoundaryShape3D)
- Licht (DirectionalLight3D, 45°, Schatten)
- Taverne (StaticBody3D, BoxMesh, Mitte der Karte)
- Gruppe (tavern)
- HitArea (Area3D) — nimmt Invasions-Schaden entgegen
- Healthbar (Sprite3D + SubViewport, groß, nur Health — kein Schild)
- Registriert bei Stats mit TavernStats Resource
- Gruppe (tavern)
- HitArea (Area3D) — nimmt Invasions-Schaden entgegen
- Healthbar (Sprite3D + SubViewport, groß, nur Health — kein Schild)
- Registriert bei Stats mit TavernStats Resource
- Spieler (Instanz von player.tscn)
- HUD (Instanz von hud.tscn)
- PortalSpawner (Node, portal_spawner.gd)
@@ -272,7 +272,7 @@ autoloads/ — Globaler Zustand
- Kollision (CapsuleShape3D, 1.8m x 0.3m)
- Mesh (CapsuleMesh)
- CameraPivot (Node3D, camera.gd)
- Camera3D
- Camera3D
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
- Ability (Node, ability.gd) — Input-Handler 1/2/3/4, emittiert ability_use_requested
- Role (Node, role/role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready)
@@ -291,25 +291,25 @@ autoloads/ — Globaler Zustand
- NavigationAgent3D
- Bewegung wird vom AI-System (ai_system.gd) gesteuert — Enemy hat keinen eigenen Movement-Node
- Healthbar (Sprite3D + SubViewport, healthbar.gd) — Health-Anzeige
- HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige
- HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe
- HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons
- HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige
- HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe
- HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons
- init.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal
- Aggro-Regeln (Werte in AggroConfig Resource):
- Aufbau:
- Schaden = Aggro (1:1), Tank 2x Multiplikator
- Heilung = 0.5x Aggro auf alle Gegner die Heiler kennen
- Aggro-Spread: 50% des Aggro an Gegner im alert_radius (10m)
- Detection-Area (10m): +1 Aggro, Alert an Nachbarn im alert_radius
- Schaden = Aggro (1:1), Tank 2x Multiplikator
- Heilung = 0.5x Aggro auf alle Gegner die Heiler kennen
- Aggro-Spread: 50% des Aggro an Gegner im alert_radius (10m)
- Detection-Area (10m): +1 Aggro, Alert an Nachbarn im alert_radius
- Kampfstatus:
- Spieler in DetectionArea → immer im Kampf (kein Decay)
- Spieler verlässt DetectionArea → 5s Combat-Timeout, dann Decay
- Schaden verursachen setzt Combat-Timer zurück
- Spieler in DetectionArea → immer im Kampf (kein Decay)
- Spieler verlässt DetectionArea → 5s Combat-Timeout, dann Decay
- Schaden verursachen setzt Combat-Timer zurück
- Abbau (nach Combat-Timeout):
- Basis: -aggro_decay/s (default 1.0)
- Exponentieller Decay basierend auf Zeit seit Kampfende (1%·2^sekunden)
- Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert
- Bei Spieler-Tod → Aggro auf 0
- Basis: -aggro_decay/s (default 1.0)
- Exponentieller Decay basierend auf Zeit seit Kampfende (1%·2^sekunden)
- Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert
- Bei Spieler-Tod → Aggro auf 0
## Boss (enemy/)
- boss.tscn — wie enemy.tscn aber größer (Mesh lila, 1.5x)
@@ -324,9 +324,9 @@ autoloads/ — Globaler Zustand
- HitArea (Area3D)
- DetectionArea (Area3D, Auto-Targeting bei Betreten)
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
- HealthbarShield (Node, healthbar_shield.gd)
- HealthbarStatus (Node, healthbar_status.gd)
- HealthbarEffects (Node, healthbar_effects.gd)
- HealthbarShield (Node, healthbar_shield.gd)
- HealthbarStatus (Node, healthbar_status.gd)
- HealthbarEffects (Node, healthbar_effects.gd)
- init.gd — Registriert bei Stats mit PortalStats Resource
- Spawnt Gegner bei HP-Schwellen (→ SpawnSystem)

View File

@@ -18,12 +18,11 @@ config/icon="res://icon.svg"
[autoload]
EventBus="*res://autoloads/event_bus.gd"
Net="*res://autoloads/net.gd"
Stats="*res://autoloads/stats.gd"
GameState="*res://autoloads/game_state.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"
TavernData="*res://autoloads/tavern_stats.gd"
SaveLoad="*res://autoloads/save_load.gd"
GameLore="*res://autoloads/game_lore.gd"
[dotnet]
@@ -51,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,266 +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="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"]
[ext_resource type="Script" path="res://systems/audio_system.gd" id="audio_system"]
[ext_resource type="Script" path="res://systems/xp_system.gd" id="xp_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="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
shape = SubResource("WorldBoundaryShape3D_1")
[node name="WallSouth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -5.25)
[node name="Mesh" type="MeshInstance3D" parent="WallSouth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallSouth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallNorth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 85.25)
[node name="Mesh" type="MeshInstance3D" parent="WallNorth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallNorth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallEast" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallEast"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallEast"]
shape = SubResource("BoxShape3D_east_west")
[node name="WallWest" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallWest"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallWest"]
shape = SubResource("BoxShape3D_east_west")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 40)
light_energy = 0.6
shadow_enabled = true
[node name="Player" parent="." instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -3)
[node name="HUD" parent="." instance=ExtResource("hud")]
[node name="Enemy1a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 15)
[node name="Enemy1b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 15)
[node name="Enemy1c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 15)
[node name="Enemy1d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 15)
[node name="Enemy2a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 30)
[node name="Enemy2b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 30)
[node name="Enemy2c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 30)
[node name="Enemy2d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 30)
[node name="Enemy3a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 45)
[node name="Enemy3b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 45)
[node name="Enemy3c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 45)
[node name="Enemy3d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 45)
[node name="Enemy4a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 60)
[node name="Enemy4b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 60)
[node name="Enemy4c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60)
[node name="Enemy4d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
[node name="Boss" parent="." 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="AudioSystem" type="Node" parent="Systems"]
script = ExtResource("audio_system")
[node name="LootSystem" type="Node" parent="Systems"]
script = ExtResource("18")
[node name="XpSystem" type="Node" parent="Systems"]
script = ExtResource("xp_system")
script = ExtResource("17")
[node name="DungeonManager" type="Node" parent="."]
script = ExtResource("dungeon_manager")
[node name="InventorySystem" type="Node" parent="Systems"]
script = ExtResource("19")
[node name="ChatSystem" type="Node" parent="Systems"]
script = ExtResource("24")
[node name="MapSystem" type="Node" parent="Systems"]
script = ExtResource("25")
[node name="AudioSystem" type="Node" parent="Systems"]
script = ExtResource("27")
[node name="EntityRoot" type="Node3D" parent="."]
[node name="Players" type="Node3D" parent="EntityRoot"]
[node name="Enemies" type="Node3D" parent="EntityRoot"]
[node name="Loot" type="Node3D" parent="EntityRoot"]
[node name="Portals" type="Node3D" parent="EntityRoot"]
[node name="Buildings" type="Node3D" parent="EntityRoot"]
[node name="Gates" type="Node3D" parent="EntityRoot"]
[node name="Npcs" type="Node3D" parent="EntityRoot"]
[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/player/player.tscn")
spawn_path = NodePath("../EntityRoot/Players")
[node name="EnemySpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/enemy/enemy.tscn")
spawn_path = NodePath("../EntityRoot/Enemies")
[node name="LootSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/loot/loot_drop.tscn")
spawn_path = NodePath("../EntityRoot/Loot")
[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)

View File

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

View File

@@ -1,16 +1,91 @@
extends Node
extends Node3D
const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn")
const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn")
const PORTAL_SCENE: PackedScene = preload("res://scenes/entities/portal/portal.tscn")
const GENERATOR: GDScript = preload("res://scenes/dungeon/dungeon_generator.gd")
@onready var players_root: Node3D = $EntityRoot/Players
@onready var dungeon_root: Node3D = $DungeonGeometry
@onready var spawn_system: Node = $Systems/SpawnSystem
var generator: Node
var data: Dictionary
func _ready() -> void:
call_deferred("_scale_dungeon")
add_to_group("dungeon")
generator = GENERATOR.new()
add_child(generator)
data = generator.generate(dungeon_root, GameState.dungeon_seed, GameState.current_wave * (5.0 if GameState.dungeon_red else 1.0))
EventBus.boss_defeated.connect(_on_boss_defeated)
Net.world_ready.connect(_on_world_ready)
Net.peer_world_loaded.connect(_on_peer_world_loaded)
if Net.is_host():
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
Net.reset_world_ready()
Net.mark_world_loaded()
func _scale_dungeon() -> void:
var variant_multiplier: float = 10.0 if GameState.last_dungeon_variant == 1 else 1.0
var total_scale: float = PlayerData.level_scale * variant_multiplier
var parent: Node = get_parent()
for child in parent.get_children():
if not child.is_in_group("enemies"):
continue
if child.is_in_group("boss"):
BossData.apply_scale(child, total_scale)
else:
EnemyData.apply_scale(child, total_scale)
func _exit_tree() -> void:
if Net.world_ready.is_connected(_on_world_ready):
Net.world_ready.disconnect(_on_world_ready)
if Net.peer_world_loaded.is_connected(_on_peer_world_loaded):
Net.peer_world_loaded.disconnect(_on_peer_world_loaded)
func _on_world_ready() -> void:
if not Net.is_host():
return
_spawn_player(1)
for id in multiplayer.get_peers():
_spawn_player(id)
_populate_dungeon()
func _populate_dungeon() -> void:
var difficulty: float = GameState.current_wave * (3.0 if GameState.dungeon_red else 1.0)
for i in range(data.rooms.size() - 1):
var room: Dictionary = data.rooms[i]
var n: int = 2 + (1 if GameState.dungeon_red else 0)
for j in range(n):
var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 1.0, randf_range(-room.size.z * 0.3, room.size.z * 0.3))
spawn_system.spawn_enemy_at(room.pos + off, GameState.dungeon_red, difficulty * 0.5)
spawn_system.spawn_boss_at(data.boss + Vector3(0, 2.0, 0), difficulty)
func _spawn_player(peer_id: int) -> void:
if players_root.get_node_or_null(str(peer_id)) != null:
return
var p: CharacterBody3D = PLAYER_SCENE.instantiate()
p.name = str(peer_id)
players_root.add_child(p, true)
p.global_position = data.spawn + Vector3(randf_range(-1, 1), 0, randf_range(-1, 1))
func _on_peer_connected(id: int) -> void:
Net.tell_peer_to_load_scene(id, GameState.SCENE_DUNGEON)
func _on_peer_world_loaded(peer_id: int) -> void:
if not Net.is_host():
return
_spawn_player(peer_id)
func _on_peer_disconnected(id: int) -> void:
var node := players_root.get_node_or_null(str(id))
if node:
node.queue_free()
func _on_boss_defeated(b: Node) -> void:
if not Net.is_host():
return
var portal: StaticBody3D = PORTAL_SCENE.instantiate()
portal.is_return = true
portal.name = "ReturnPortal"
var portals_root: Node3D = $EntityRoot/Portals
portals_root.add_child(portal, true)
var spawn_pos: Vector3 = (b as Node3D).global_position if b is Node3D else data.boss
portal.global_position = spawn_pos + Vector3(0, 1, 0)
EventBus.portal_spawned.emit(portal)
var t := get_tree().create_timer(3.0)
t.timeout.connect(_auto_return.bind(portal))
func _auto_return(portal: Node) -> void:
if is_instance_valid(portal):
portal.queue_free()
GameState.change_scene(GameState.SCENE_WORLD)

View File

@@ -1 +1 @@
uid://bfkxrflfn5qx4
uid://civwsilci7nlx

View File

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

View File

@@ -1,20 +0,0 @@
[gd_resource type="Resource" script_class="BossStats" load_steps=2 format=3]
[ext_resource type="Script" path="res://scenes/enemy/boss_stats.gd" id="1"]
[resource]
script = ExtResource("1")
max_health = 500.0
health_regen = 0.0
max_shield = 100.0
shield_regen_delay = 5.0
shield_regen_time = 8.0
speed = 3.0
attack_range = 2.0
attack_cooldown = 1.5
attack_damage = 5.0
regen_fast = 0.1
regen_slow = 0.01
aggro_decay = 1.0
portal_radius = 10.0
alert_radius = 10.0

View File

@@ -1,11 +0,0 @@
extends Node
@onready var entity: CharacterBody3D = get_parent()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_detected.emit(entity, body)
func _on_detection_area_body_exited(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_lost.emit(entity, body)

View File

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

View File

@@ -1,96 +0,0 @@
[gd_scene format=3 uid="uid://db8pa55ev4l4a"]
[ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"]
[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"]
[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"]
[ext_resource type="PackedScene" path="res://assets/models/characters/Skeleton_Minion.glb" id="9"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.4
height = 1.5
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
radius = 0.4
height = 1.5
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 10.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
bg_color = Color(0.3, 0.1, 0.1, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
bg_color = Color(0.2, 0.8, 0.2, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1)
[node name="Enemy" type="CharacterBody3D" unique_id=1724620529]
script = ExtResource("1")
stats = ExtResource("8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038]
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="Node3D" parent="." unique_id=1598094615]
[node name="Model" parent="Mesh" instance=ExtResource("9")]
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, -0.75, 0)
[node name="HitArea" type="Area3D" parent="." unique_id=893463784]
collision_layer = 4
collision_mask = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962]
shape = SubResource("CapsuleShape3D_2")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945]
[node name="Detection" type="Node" parent="." unique_id=534240144]
script = ExtResource("2")
[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598]
collision_layer = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347]
shape = SubResource("SphereShape3D_1")
[node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
billboard = 1
[node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718]
transparent_bg = true
size = Vector2i(104, 29)
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848]
offset_right = 104.0
offset_bottom = 29.0
color = Color(1, 0.9, 0.2, 1)
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403]
offset_left = 2.0
offset_top = 2.0
offset_right = 102.0
offset_bottom = 12.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
value = 100.0
show_percentage = false
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036]
offset_left = 2.0
offset_top = 15.0
offset_right = 102.0
offset_bottom = 27.0
theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill")
max_value = 50.0
value = 50.0
show_percentage = false
[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"]
[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"]

View File

@@ -1,12 +0,0 @@
extends BaseStats
class_name EnemyStats
@export var speed := 3.0
@export var attack_range := 2.0
@export var attack_cooldown := 1.5
@export var attack_damage := 5.0
@export var regen_fast := 0.10
@export var regen_slow := 0.01
@export var aggro_decay := 1.0
@export var portal_radius := 10.0
@export var alert_radius := 10.0

View File

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

View File

@@ -1,7 +0,0 @@
[gd_resource type="Resource" script_class="EnemyStats" format=3 uid="uid://cj1shmjwf0xeo"]
[ext_resource type="Script" uid="uid://bh2uuuvl30y0x" path="res://scenes/enemy/enemy_stats.gd" id="1"]
[resource]
script = ExtResource("1")
max_shield = 50.0

View File

@@ -1,101 +0,0 @@
extends CharacterBody3D
const SKELETON_WARRIOR: PackedScene = preload("res://assets/models/characters/Skeleton_Warrior.glb")
const SKELETON_MAGE: PackedScene = preload("res://assets/models/characters/Skeleton_Mage.glb")
@export var stats: EnemyStats
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
var spawn_scale: float = 1.0
var anim_player: AnimationPlayer = null
var current_anim: String = ""
var attack_lock_until: float = 0.0
var is_dying: bool = false
func _ready() -> void:
add_to_group("enemies")
if is_in_group("boss"):
BossData.register(self, stats, spawn_scale)
BossData.set_stat(self, "spawn_position", global_position)
_swap_model(SKELETON_MAGE, 1.3)
else:
EnemyData.register(self, stats, spawn_scale)
EnemyData.set_stat(self, "spawn_position", global_position)
EventBus.entity_died.connect(_on_entity_died)
EventBus.attack_executed.connect(_on_attack_executed)
call_deferred("_check_variant")
call_deferred("_init_anim")
func _init_anim() -> void:
anim_player = get_node_or_null("Mesh/Model/AnimationPlayer")
_play_anim("Idle")
func _check_variant() -> void:
if is_in_group("boss"):
return
if is_in_group("red_enemies") or is_in_group("invasion"):
_swap_model(SKELETON_WARRIOR, 1.0)
anim_player = get_node_or_null("Mesh/Model/AnimationPlayer")
_play_anim("Idle")
func _swap_model(new_scene: PackedScene, scale_factor: float = 1.0) -> void:
var mesh: Node3D = get_node_or_null("Mesh")
if not mesh:
return
var old: Node = mesh.get_node_or_null("Model")
if old:
old.queue_free()
var new_model: Node3D = new_scene.instantiate()
new_model.name = "Model"
new_model.scale = Vector3(scale_factor, scale_factor, scale_factor)
new_model.position = Vector3(0, -0.75, 0)
new_model.rotation.y = PI
mesh.add_child(new_model)
func _exit_tree() -> void:
if is_in_group("boss"):
BossData.deregister(self)
else:
EnemyData.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return
is_dying = true
_play_anim("Death_A", false)
get_tree().create_timer(1.0).timeout.connect(queue_free)
func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void:
if attacker != self:
return
_play_anim("1H_Melee_Attack_Chop", false)
attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5
func _process(_delta: float) -> void:
if is_dying:
return
var now: float = Time.get_ticks_msec() / 1000.0
if now < attack_lock_until:
return
if velocity.length() > 0.1:
_play_anim("Running_A")
else:
_play_anim("Idle")
func _play_anim(anim_name: String, loop: bool = true) -> void:
if not anim_player:
return
if current_anim == anim_name:
return
if not anim_player.has_animation(anim_name):
return
var anim: Animation = anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
anim_player.play(anim_name)
current_anim = anim_name
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()

View File

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

View File

@@ -0,0 +1,36 @@
extends StaticBody3D
@export var building_id: StringName = &""
@onready var mesh: MeshInstance3D = $Mesh
@onready var collision: CollisionShape3D = $Collision
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("buildings")
apply_building(building_id)
func apply_building(id: StringName) -> void:
building_id = id
var data: Building = _load_building(id)
if data == null:
return
var box: BoxMesh = mesh.mesh as BoxMesh
if box:
box.size = data.size
var shape: BoxShape3D = collision.shape as BoxShape3D
if shape:
shape.size = data.size
collision.position = Vector3(0.0, data.size.y * 0.5, 0.0)
mesh.position = Vector3(0.0, data.size.y * 0.5, 0.0)
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = data.color
mesh.material_override = mat
static func _load_building(id: StringName) -> Building:
var path := "res://resources/buildings/%s.tres" % str(id)
if ResourceLoader.exists(path):
return load(path) as Building
return null

View File

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

View File

@@ -0,0 +1,22 @@
[gd_scene load_steps=4 format=3 uid="uid://b0building001"]
[ext_resource type="Script" path="res://scenes/entities/building/building.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape3D_1"]
size = Vector3(1, 1, 1)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(1, 1, 1)
[node name="Building" type="StaticBody3D"]
collision_layer = 16
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
shape = SubResource("BoxShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
mesh = SubResource("BoxMesh_1")

View File

@@ -0,0 +1,170 @@
extends CharacterBody3D
const GRAVITY: float = 18.0
@export var stats_resource: EnemyStats
@export var is_boss: bool = false
@onready var nav: NavigationAgent3D = $NavAgent
@onready var mesh_holder: Node3D = $MeshHolder
@onready var collision: CollisionShape3D = $Collision
@onready var detection: Area3D = $DetectionArea
@onready var sync: MultiplayerSynchronizer = $Synchronizer
@onready var healthbar: MeshInstance3D = $Healthbar
@onready var name_label: Label3D = $NameLabel
var origin: Vector3 = Vector3.ZERO
var attack_cd: float = 0.0
var dead: bool = false
var invasion_target: Node = null
@export var sync_position: Vector3 = Vector3.ZERO
@export var sync_yaw: float = 0.0
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
if is_boss:
add_to_group("boss")
add_to_group("enemies")
if stats_resource == null:
stats_resource = EnemyStats.new()
Stats.register(self, stats_resource)
origin = global_position
detection.body_entered.connect(_on_body_entered)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
name_label.text = "Boss" if is_boss else "Enemy"
if is_boss:
name_label.text = "Boss"
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
mesh.scale = Vector3(1.5, 1.5, 1.5)
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.6, 0.2, 0.8)
mesh.material_override = mat
func _exit_tree() -> void:
Stats.deregister(self)
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
rotation.y = lerp_angle(rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
return
if dead:
return
if not is_on_floor():
velocity.y -= GRAVITY * delta
attack_cd = max(0.0, attack_cd - delta)
var target: Node = _get_target()
if target == null:
_return_to_origin(delta)
else:
_chase_or_attack(target, delta)
move_and_slide()
sync_position = global_position
sync_yaw = rotation.y
func _get_target() -> Node:
var aggro: Node = get_node_or_null("/root/World/Systems/AggroSystem")
if aggro == null:
aggro = get_node_or_null("/root/Dungeon/Systems/AggroSystem")
if aggro and aggro.has_method("target_for"):
var t: Node = aggro.target_for(self)
if t:
return t
if invasion_target and is_instance_valid(invasion_target):
return invasion_target
return null
func _chase_or_attack(target: Node, delta: float) -> void:
var t_pos: Vector3 = (target as Node3D).global_position
var d: float = global_position.distance_to(t_pos)
var attack_range: float = float(Stats.get_stat(self, "attack_range", 2.0))
if d <= attack_range:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var look := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z)
if look.length() > 0.01:
rotation.y = atan2(look.x, look.z)
if attack_cd <= 0.0:
attack_cd = float(Stats.get_stat(self, "attack_cooldown", 1.5))
var dmg: float = float(Stats.get_stat(self, "attack_damage", 5.0))
EventBus.damage_requested.emit(self, target, dmg, Element.NONE)
else:
var dir := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0))
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _return_to_origin(delta: float) -> void:
var d: float = global_position.distance_to(origin)
if d < 0.5:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var max_hp: float = float(Stats.get_stat(self, "max_health", 100.0))
var hp: float = float(Stats.get_stat(self, "health", 0.0))
if hp > 0.0 and hp < max_hp:
Stats.set_stat(self, "health", min(max_hp, hp + max_hp * 0.20 * delta))
EventBus.health_changed.emit(self, Stats.get_stat(self, "health"), max_hp)
else:
var dir: Vector3 = Vector3(origin.x - global_position.x, 0.0, origin.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0)) * 1.5
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _on_body_entered(body: Node) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if body.is_in_group("player") and not dead:
EventBus.enemy_detected.emit(self, body)
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
if healthbar:
healthbar.scale.x = max(0.01, ratio)
func _on_entity_died(entity: Node) -> void:
if entity != self or dead:
return
dead = true
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
_on_death.rpc()
var loot: Node = get_node_or_null("/root/World/Systems/LootSystem")
if loot == null:
loot = get_node_or_null("/root/Dungeon/Systems/LootSystem")
if loot and loot.has_method("drop_loot_for"):
loot.drop_loot_for(self, global_position)
var xp_sys: Node = get_node_or_null("/root/World/Systems/XpSystem")
if xp_sys == null:
xp_sys = get_node_or_null("/root/Dungeon/Systems/XpSystem")
if xp_sys and xp_sys.has_method("award_for_enemy"):
xp_sys.award_for_enemy(self)
if is_boss:
EventBus.boss_defeated.emit(self)
var t := get_tree().create_timer(2.0)
t.timeout.connect(func():
if is_instance_valid(self):
queue_free())
@rpc("any_peer", "reliable", "call_local")
func _on_death() -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
dead = true
collision.disabled = true
modulate_alpha(0.4)
func modulate_alpha(a: float) -> void:
for child in mesh_holder.get_children():
if child is MeshInstance3D and child.material_override:
var c: Color = child.material_override.albedo_color
c.a = a
child.material_override.albedo_color = c

View File

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

View File

@@ -0,0 +1,76 @@
[gd_scene load_steps=8 format=3 uid="uid://b0enemy00001"]
[ext_resource type="Script" path="res://scenes/entities/enemy/enemy.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.6
radius = 0.4
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.6
radius = 0.4
[sub_resource type="StandardMaterial3D" id="Mat_1"]
albedo_color = Color(0.6, 0.6, 0.6, 1)
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 12.0
[sub_resource type="QuadMesh" id="QuadMesh_1"]
size = Vector2(1.0, 0.12)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.9, 0.2, 0.2, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"]
properties/0/path = NodePath(".:sync_position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:sync_yaw")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Enemy" type="CharacterBody3D"]
collision_layer = 4
collision_mask = 1
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="MeshHolder" type="Node3D" parent="."]
[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
mesh = SubResource("CapsuleMesh_1")
material_override = SubResource("Mat_1")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
billboard = 1
text = "Enemy"
font_size = 20
outline_size = 3
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.95, 0)
mesh = SubResource("QuadMesh_1")
material_override = SubResource("HBMat")
gi_mode = 0
[node name="DetectionArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="DetectionShape" type="CollisionShape3D" parent="DetectionArea"]
shape = SubResource("SphereShape3D_1")
[node name="NavAgent" type="NavigationAgent3D" parent="."]
path_desired_distance = 0.5
target_desired_distance = 0.5
[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_1")

View File

@@ -0,0 +1,85 @@
extends StaticBody3D
@export var stats_resource: GateStats
@export var is_red: bool = false
@onready var mesh: MeshInstance3D = $Mesh
@onready var healthbar: MeshInstance3D = $Healthbar
@onready var name_label: Label3D = $NameLabel
@onready var spawn_point: Node3D = $SpawnPoint
var spawn_timer: float = 0.0
var spawned_count: int = 0
var dead: bool = false
var attacked: bool = false
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("gates")
if is_red:
add_to_group("red_gate")
if stats_resource == null:
stats_resource = GateStats.new()
if is_red:
stats_resource.max_health = 600.0
stats_resource.spawn_count = 8
else:
stats_resource.max_health = 200.0
stats_resource.spawn_count = 5
stats_resource.is_red = is_red
Stats.register(self, stats_resource)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
if is_red:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.95, 0.2, 0.15)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.4, 0.2)
mat.emission_energy_multiplier = 0.6
mesh.material_override = mat
name_label.text = "Red Gate"
else:
name_label.text = "Gate"
set_physics_process(true)
func _exit_tree() -> void:
Stats.deregister(self)
func _physics_process(delta: float) -> void:
if dead:
return
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if not attacked:
return
spawn_timer = max(0.0, spawn_timer - delta)
if spawn_timer <= 0.0 and spawned_count < int(Stats.get_stat(self, "spawn_count", 5)):
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
if spawn_sys and spawn_sys.has_method("spawn_enemy_at"):
spawn_sys.spawn_enemy_at(spawn_point.global_position + Vector3(randf_range(-1.5, 1.5), 0.0, randf_range(-1.5, 1.5)), is_red)
spawned_count += 1
spawn_timer = float(Stats.get_stat(self, "spawn_interval", 4.0))
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
if not attacked and current < max:
attacked = true
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
healthbar.scale.x = max(0.01, ratio * 2.0)
func _on_entity_died(entity: Node) -> void:
if entity != self or dead:
return
dead = true
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
if spawn_sys and spawn_sys.has_method("spawn_portal_at"):
spawn_sys.spawn_portal_at(global_position, is_red)
EventBus.gate_destroyed.emit(self)
var t := get_tree().create_timer(0.5)
t.timeout.connect(func():
if is_instance_valid(self):
queue_free())

View File

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

View File

@@ -0,0 +1,53 @@
[gd_scene load_steps=7 format=3 uid="uid://b0gate0001"]
[ext_resource type="Script" path="res://scenes/entities/gate/gate.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape3D_1"]
size = Vector3(2, 3, 2)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(2, 3, 2)
[sub_resource type="StandardMaterial3D" id="GateMat"]
albedo_color = Color(0.4, 0.4, 0.55, 1)
emission_enabled = true
emission = Color(0.2, 0.2, 0.4, 1)
emission_energy_multiplier = 0.4
[sub_resource type="QuadMesh" id="QuadMesh_HB"]
size = Vector2(1.0, 0.18)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.95, 0.4, 0.2, 1)
[node name="Gate" type="StaticBody3D"]
collision_layer = 4
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
shape = SubResource("BoxShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("GateMat")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0)
billboard = 1
text = "Gate"
font_size = 22
outline_size = 3
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.5, 0)
mesh = SubResource("QuadMesh_HB")
material_override = SubResource("HBMat")
gi_mode = 0
[node name="SpawnPoint" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)

View File

@@ -0,0 +1,39 @@
extends Area3D
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@export var item_id: StringName = &""
@export var amount: int = 1
var rotation_speed: float = 1.5
var bob: float = 0.0
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("loot")
body_entered.connect(_on_body_entered)
if item_id != &"":
label.text = "%s x%d" % [str(item_id), amount]
else:
label.text = "Loot"
func _process(delta: float) -> void:
bob += delta
mesh.rotation.y += rotation_speed * delta
mesh.position.y = 0.5 + sin(bob * 2.0) * 0.1
func _on_body_entered(body: Node) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if not body.is_in_group("player"):
return
var inv: Node = get_node_or_null("/root/World/Systems/InventorySystem")
if inv == null:
inv = get_node_or_null("/root/Dungeon/Systems/InventorySystem")
if inv and inv.has_method("add_item"):
inv.add_item(body, item_id, amount)
EventBus.item_picked_up.emit(body, item_id)
queue_free()

View File

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

View File

@@ -0,0 +1,35 @@
[gd_scene load_steps=4 format=3 uid="uid://b0loot0001"]
[ext_resource type="Script" path="res://scenes/entities/loot/loot_drop.gd" id="1"]
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(0.4, 0.4, 0.4)
[sub_resource type="StandardMaterial3D" id="LootMat"]
albedo_color = Color(0.95, 0.85, 0.2, 1)
emission_enabled = true
emission = Color(1.0, 0.9, 0.4, 1)
emission_energy_multiplier = 0.6
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 1.2
[node name="LootDrop" type="Area3D"]
collision_layer = 0
collision_mask = 2
script = ExtResource("1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("LootMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
billboard = 1
text = "Loot"
font_size = 18
outline_size = 3
[node name="PickupShape" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_1")

View File

@@ -0,0 +1,52 @@
extends StaticBody3D
@export var profile: NpcProfile
@export var profile_id: StringName = &""
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@onready var prompt: Label3D = $Prompt
@onready var interact_area: Area3D = $InteractArea
var nearby_player: Node = null
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("npc")
if profile == null and profile_id != &"":
var path := "res://resources/npcs/%s.tres" % str(profile_id)
if ResourceLoader.exists(path):
profile = load(path) as NpcProfile
if profile == null:
profile = NpcProfile.new()
profile.display_name = "Villager"
profile.lore = "A simple villager."
profile.personality = "Friendly, curious."
profile.fallback_text = "Hello there!"
label.text = profile.display_name
prompt.visible = false
if profile.color != Color.WHITE:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = profile.color
mesh.material_override = mat
interact_area.body_entered.connect(_on_player_near)
interact_area.body_exited.connect(_on_player_left)
func _process(_delta: float) -> void:
if nearby_player and nearby_player.is_multiplayer_authority():
prompt.visible = true
if Input.is_action_just_pressed("interact") and not nearby_player.ui_capturing:
EventBus.dialog_opened.emit(nearby_player, self)
else:
prompt.visible = false
func _on_player_near(body: Node) -> void:
if body.is_in_group("player"):
if body.is_multiplayer_authority():
nearby_player = body
func _on_player_left(body: Node) -> void:
if body == nearby_player:
nearby_player = null

View File

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

View File

@@ -0,0 +1,53 @@
[gd_scene load_steps=6 format=3 uid="uid://b0npc0001"]
[ext_resource type="Script" path="res://scenes/entities/npc/npc.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.7
radius = 0.35
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.7
radius = 0.35
[sub_resource type="StandardMaterial3D" id="NpcMat"]
albedo_color = Color(0.85, 0.7, 0.5, 1)
[sub_resource type="SphereShape3D" id="InteractShape"]
radius = 2.5
[node name="Npc" type="StaticBody3D"]
collision_layer = 0
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
mesh = SubResource("CapsuleMesh_1")
material_override = SubResource("NpcMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
billboard = 1
text = "Villager"
font_size = 22
outline_size = 3
modulate = Color(0.95, 0.85, 0.5, 1)
[node name="Prompt" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.55, 0)
billboard = 1
text = "[E] Talk"
font_size = 16
outline_size = 2
[node name="InteractArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="InteractShape" type="CollisionShape3D" parent="InteractArea"]
shape = SubResource("InteractShape")

View File

@@ -0,0 +1,246 @@
extends CharacterBody3D
const GRAVITY: float = 18.0
const MOUSE_SENS: float = 0.0035
@export var stats_resource: PlayerStats
@onready var pivot: Node3D = $Pivot
@onready var pitch_pivot: Node3D = $Pivot/PitchPivot
@onready var camera: Camera3D = $Pivot/PitchPivot/Camera
@onready var mesh_holder: Node3D = $MeshHolder
@onready var collision: CollisionShape3D = $Collision
@onready var sync: MultiplayerSynchronizer = $Synchronizer
@onready var name_label: Label3D = $NameLabel
var peer_id: int = 1
var role: int = GameState.ROLE_DAMAGE
var look_dragging: bool = false
var current_target: Node = null
var dead: bool = false
var ui_capturing: bool = false
var build_mode: bool = false
@export var sync_position: Vector3 = Vector3.ZERO
@export var sync_velocity: Vector3 = Vector3.ZERO
@export var sync_yaw: float = 0.0
@export var sync_role: int = GameState.ROLE_DAMAGE
func _enter_tree() -> void:
peer_id = name.to_int()
set_multiplayer_authority(peer_id)
func _ready() -> void:
add_to_group("player")
if stats_resource == null:
stats_resource = PlayerStats.new()
Stats.register(self, stats_resource)
Stats.restore_player(peer_id, self)
Stats.set_stat(self, "role", role)
name_label.text = Net.player_names.get(peer_id, "P%d" % peer_id)
EventBus.entity_died.connect(_on_entity_died_clear_target)
if is_multiplayer_authority():
camera.current = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
camera.current = false
_apply_role_visual(role)
func _on_entity_died_clear_target(entity: Node) -> void:
if entity == current_target:
current_target = null
func _set_target(t: Node) -> void:
current_target = t
EventBus.target_changed.emit(self, t)
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and not multiplayer.is_server():
_request_target.rpc_id(1, String(t.get_path()) if t else "")
@rpc("any_peer", "reliable")
func _request_target(path_str: String) -> void:
if not multiplayer.is_server():
return
var t: Node = get_node_or_null(NodePath(path_str)) if path_str != "" else null
current_target = t
func _exit_tree() -> void:
Stats.cache_player(peer_id, self)
Stats.deregister(self)
func _input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
if ui_capturing:
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
look_dragging = event.pressed
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if look_dragging else Input.MOUSE_MODE_VISIBLE
elif event is InputEventMouseMotion and look_dragging:
pivot.rotate_y(-event.relative.x * MOUSE_SENS)
pitch_pivot.rotate_x(-event.relative.y * MOUSE_SENS)
pitch_pivot.rotation.x = clamp(pitch_pivot.rotation.x, -1.2, 0.2)
func _unhandled_input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
if dead:
return
if build_mode:
return
if ui_capturing:
return
if event.is_action_pressed("class_tank"):
_request_role(GameState.ROLE_TANK)
elif event.is_action_pressed("class_damage"):
_request_role(GameState.ROLE_DAMAGE)
elif event.is_action_pressed("class_healer"):
_request_role(GameState.ROLE_HEALER)
elif event.is_action_pressed("ability_1"):
EventBus.ability_use_requested.emit(self, 0)
elif event.is_action_pressed("ability_2"):
EventBus.ability_use_requested.emit(self, 1)
elif event.is_action_pressed("ability_3"):
EventBus.ability_use_requested.emit(self, 2)
elif event.is_action_pressed("ability_4"):
EventBus.ability_use_requested.emit(self, 3)
elif event.is_action_pressed("target_next"):
var nt := _cycle_target()
_set_target(nt)
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed and not look_dragging:
var t := _pick_target_under_mouse()
if t:
_set_target(t)
func _physics_process(delta: float) -> void:
if is_multiplayer_authority():
if not dead:
_process_local(delta)
sync_position = global_position
sync_velocity = velocity
sync_yaw = pivot.rotation.y
sync_role = role
else:
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
pivot.rotation.y = lerp_angle(pivot.rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
if sync_role != role:
role = sync_role
_apply_role_visual(role)
func _process_local(delta: float) -> void:
if not is_on_floor():
velocity.y -= GRAVITY * delta
var move_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back") if not ui_capturing else Vector2.ZERO
var speed: float = float(Stats.get_stat(self, "speed", 5.0))
var basis_y := Basis(Vector3.UP, pivot.rotation.y)
var direction := basis_y * Vector3(move_dir.x, 0.0, move_dir.y)
if direction.length() > 0.01:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
var look_dir := Vector3(velocity.x, 0.0, velocity.z).normalized()
var target_basis := Basis.looking_at(look_dir, Vector3.UP)
mesh_holder.basis = mesh_holder.basis.slerp(target_basis, clamp(delta * 12.0, 0.0, 1.0))
else:
velocity.x = move_toward(velocity.x, 0.0, speed * 6.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, speed * 6.0 * delta)
if Input.is_action_just_pressed("jump") and is_on_floor() and not ui_capturing:
velocity.y = float(Stats.get_stat(self, "jump_velocity", 4.5))
move_and_slide()
func _request_role(new_role: int) -> void:
_set_role.rpc_id(1, new_role)
@rpc("any_peer", "reliable", "call_local")
func _set_role(new_role: int) -> void:
if not multiplayer.is_server():
return
if new_role == role:
return
role = new_role
Stats.set_stat(self, "role", role)
_apply_role.rpc(role)
@rpc("any_peer", "reliable", "call_local")
func _apply_role(new_role: int) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
role = new_role
if Stats.has(self):
Stats.set_stat(self, "role", role)
_apply_role_visual(role)
EventBus.role_changed.emit(self, role)
func _apply_role_visual(r: int) -> void:
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
var mat: StandardMaterial3D = mesh.get_active_material(0).duplicate() if mesh.get_active_material(0) else StandardMaterial3D.new()
match r:
GameState.ROLE_TANK:
mat.albedo_color = Color(0.3, 0.5, 0.95)
GameState.ROLE_DAMAGE:
mat.albedo_color = Color(0.95, 0.3, 0.3)
GameState.ROLE_HEALER:
mat.albedo_color = Color(0.4, 0.85, 0.4)
mesh.material_override = mat
@rpc("any_peer", "reliable", "call_local")
func set_dead(value: bool) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
dead = value
visible = not value
collision.disabled = value
if value:
velocity = Vector3.ZERO
@rpc("any_peer", "reliable", "call_local")
func teleport_to(pos: Vector3) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
global_position = pos
sync_position = pos
func set_ui_capturing(v: bool) -> void:
ui_capturing = v
func set_build_mode(v: bool) -> void:
build_mode = v
func _cycle_target() -> Node:
if current_target != null and not is_instance_valid(current_target):
current_target = null
var candidates: Array = []
for n in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(n):
candidates.append(n)
for n in get_tree().get_nodes_in_group("portals"):
if is_instance_valid(n):
candidates.append(n)
for n in get_tree().get_nodes_in_group("gates"):
if is_instance_valid(n):
candidates.append(n)
if candidates.is_empty():
current_target = null
return null
candidates.sort_custom(func(a, b):
return (a as Node3D).global_position.distance_to(global_position) < (b as Node3D).global_position.distance_to(global_position))
if current_target == null or not current_target in candidates:
current_target = candidates[0]
else:
var idx: int = candidates.find(current_target)
current_target = candidates[(idx + 1) % candidates.size()]
return current_target
func _pick_target_under_mouse() -> Node:
var mouse := get_viewport().get_mouse_position()
var from := camera.project_ray_origin(mouse)
var to := from + camera.project_ray_normal(mouse) * 100.0
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 0xFFFFFFFF
query.exclude = [self]
var hit := space.intersect_ray(query)
if hit.is_empty():
return null
var n: Node = hit.collider
while n != null and not (n.is_in_group("enemies") or n.is_in_group("portals") or n.is_in_group("gates") or n.is_in_group("npc")):
n = n.get_parent()
return n

View File

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

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=8 format=3 uid="uid://b0player00001"]
[ext_resource type="Script" path="res://scenes/entities/player/player.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.8
radius = 0.35
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.8
radius = 0.35
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.3, 0.55, 0.95, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"]
properties/0/path = NodePath(".:sync_position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:sync_yaw")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Player" type="CharacterBody3D"]
collision_layer = 2
collision_mask = 1
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="MeshHolder" type="Node3D" parent="."]
[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
mesh = SubResource("CapsuleMesh_1")
surface_material_override/0 = SubResource("StandardMaterial3D_1")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0)
billboard = 1
text = "Player"
font_size = 24
outline_size = 4
[node name="Pivot" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="PitchPivot" type="Node3D" parent="Pivot"]
[node name="Camera" type="Camera3D" parent="Pivot/PitchPivot"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 5)
current = false
fov = 70.0
[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_1")

View File

@@ -0,0 +1,84 @@
extends StaticBody3D
@export var stats_resource: PortalStats
@export var is_red: bool = false
@export var is_return: bool = false
@onready var mesh: MeshInstance3D = $Mesh
@onready var name_label: Label3D = $NameLabel
@onready var enter_area: Area3D = $EnterArea
var triggered: bool = false
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("portals")
if is_red:
add_to_group("red_portal")
if stats_resource == null:
stats_resource = PortalStats.new()
stats_resource.max_health = 1.0
stats_resource.is_red = is_red
Stats.register(self, stats_resource)
enter_area.body_entered.connect(_on_body_entered)
if is_return:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.85, 0.3)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.8, 0.2)
mat.emission_energy_multiplier = 1.2
mesh.material_override = mat
name_label.text = "Zurück"
elif is_red:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.95, 0.2, 0.2)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.4, 0.3)
mat.emission_energy_multiplier = 1.5
mesh.material_override = mat
name_label.text = "Red Portal"
else:
name_label.text = "Portal"
func _exit_tree() -> void:
Stats.deregister(self)
func _on_body_entered(body: Node) -> void:
if not body.is_in_group("player"):
return
if not body.is_multiplayer_authority():
return
if triggered:
return
triggered = true
EventBus.portal_entered.emit(self, body)
if is_return:
_request_return.rpc_id(1)
else:
_request_enter.rpc_id(1, is_red, global_position)
@rpc("any_peer", "reliable", "call_local")
func _request_enter(red: bool, return_pos: Vector3) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
var seed: int = randi()
_do_enter.rpc(seed, red, return_pos)
@rpc("authority", "reliable", "call_local")
func _do_enter(seed: int, red: bool, return_pos: Vector3) -> void:
GameState.dungeon_seed = seed
GameState.dungeon_red = red
GameState.portal_return_position = return_pos
GameState.change_scene(GameState.SCENE_DUNGEON)
@rpc("any_peer", "reliable", "call_local")
func _request_return() -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
_do_return.rpc()
@rpc("authority", "reliable", "call_local")
func _do_return() -> void:
GameState.change_scene(GameState.SCENE_WORLD)

View File

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

View File

@@ -0,0 +1,49 @@
[gd_scene load_steps=6 format=3 uid="uid://b0portal0001"]
[ext_resource type="Script" path="res://scenes/entities/portal/portal.gd" id="1"]
[sub_resource type="CylinderShape3D" id="CylShape_1"]
height = 0.4
radius = 1.5
[sub_resource type="CylinderMesh" id="CylMesh_1"]
top_radius = 1.5
bottom_radius = 1.5
height = 3.0
[sub_resource type="StandardMaterial3D" id="PortalMat"]
albedo_color = Color(0.3, 0.5, 0.95, 1)
emission_enabled = true
emission = Color(0.5, 0.7, 1.0, 1)
emission_energy_multiplier = 1.5
[sub_resource type="SphereShape3D" id="SphereEnter"]
radius = 1.8
[node name="Portal" type="StaticBody3D"]
collision_layer = 8
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, 0)
shape = SubResource("CylShape_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
mesh = SubResource("CylMesh_1")
material_override = SubResource("PortalMat")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0)
billboard = 1
text = "Portal"
font_size = 22
outline_size = 3
[node name="EnterArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="EnterShape" type="CollisionShape3D" parent="EnterArea"]
shape = SubResource("SphereEnter")

View File

@@ -0,0 +1,36 @@
extends StaticBody3D
@export var stats_resource: VillageStats
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@onready var healthbar: MeshInstance3D = $Healthbar
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("village")
if stats_resource == null:
stats_resource = VillageStats.new()
stats_resource.max_health = 1000.0
Stats.register(self, stats_resource)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
label.text = "Village"
func _exit_tree() -> void:
Stats.deregister(self)
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
EventBus.village_damaged.emit(current, max)
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
healthbar.scale.x = max(0.01, ratio * 4.0)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return
EventBus.village_destroyed.emit()
EventBus.game_over.emit()

View File

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

View File

@@ -0,0 +1,47 @@
[gd_scene load_steps=6 format=3 uid="uid://b0village001"]
[ext_resource type="Script" path="res://scenes/entities/village/village.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape_1"]
size = Vector3(6, 4, 6)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(6, 4, 6)
[sub_resource type="StandardMaterial3D" id="VillageMat"]
albedo_color = Color(0.55, 0.4, 0.25, 1)
[sub_resource type="QuadMesh" id="QuadMesh_HB"]
size = Vector2(1.0, 0.25)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.4, 0.85, 0.4, 1)
[node name="Village" type="StaticBody3D"]
collision_layer = 1
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
shape = SubResource("BoxShape_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("VillageMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 0)
billboard = 1
text = "Village"
font_size = 28
outline_size = 4
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.0, 0)
mesh = SubResource("QuadMesh_HB")
material_override = SubResource("HBMat")
gi_mode = 0

509
scenes/hud/hud.gd Normal file
View File

@@ -0,0 +1,509 @@
extends CanvasLayer
@onready var hp_bar: ProgressBar = %HpBar
@onready var hp_label: Label = %HpLabel
@onready var shield_bar: ProgressBar = %ShieldBar
@onready var shield_label: Label = %ShieldLabel
@onready var xp_bar: ProgressBar = %XpBar
@onready var level_label: Label = %LevelLabel
@onready var wave_label: Label = %WaveLabel
@onready var timer_label: Label = %WaveTimer
@onready var village_bar: ProgressBar = %VillageBar
@onready var role_icon: Panel = %RoleIcon
@onready var role_label: Label = %RoleLabel
@onready var ability_box: HBoxContainer = %AbilityBox
@onready var death_overlay: Control = %DeathOverlay
@onready var death_label: Label = %DeathLabel
@onready var chat_log: RichTextLabel = %ChatLog
@onready var chat_input: LineEdit = %ChatInput
@onready var inventory_panel: Control = %InventoryPanel
@onready var inventory_list: VBoxContainer = %InventoryList
@onready var crafting_panel: Control = %CraftingPanel
@onready var crafting_list: VBoxContainer = %CraftingList
@onready var build_panel: Control = %BuildPanel
@onready var build_list: HBoxContainer = %BuildList
@onready var dialog_panel: Control = %DialogPanel
@onready var dialog_npc: Label = %DialogNpc
@onready var dialog_log: RichTextLabel = %DialogLog
@onready var dialog_input: LineEdit = %DialogInput
@onready var map_panel: Control = %MapPanel
@onready var map_canvas: Control = %MapCanvas
@onready var minimap_canvas: Control = %MinimapCanvas
@onready var pause_panel: Control = %PausePanel
@onready var game_over_overlay: Control = %GameOverOverlay
@onready var ability_buttons: Array = []
var local_player: Node = null
var dialog_npc_node: Node = null
var build_selected: int = 0
var build_rotation: float = 0.0
var build_preview: MeshInstance3D = null
var build_active: bool = false
var _minimap_accum: float = 0.0
func _ready() -> void:
add_to_group("dialog_ui")
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
EventBus.cooldown_tick.connect(_on_cooldown_tick)
EventBus.role_changed.connect(_on_role_changed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.entity_respawned.connect(_on_respawned)
EventBus.wave_started.connect(_on_wave_started)
EventBus.wave_timer_tick.connect(_on_wave_tick)
EventBus.village_damaged.connect(_on_village_damaged)
EventBus.village_destroyed.connect(_on_village_destroyed)
EventBus.invasion_started.connect(_on_invasion_started)
EventBus.xp_gained.connect(_on_xp_gained)
EventBus.level_up.connect(_on_level_up)
EventBus.dialog_opened.connect(_on_dialog_opened)
EventBus.inventory_changed.connect(_on_inventory_changed)
EventBus.chat_message.connect(_on_chat)
chat_input.text_submitted.connect(_on_chat_submitted)
dialog_input.text_submitted.connect(_on_dialog_submitted)
death_overlay.visible = false
inventory_panel.visible = false
crafting_panel.visible = false
build_panel.visible = false
dialog_panel.visible = false
map_panel.visible = false
pause_panel.visible = false
game_over_overlay.visible = false
set_process(true)
_wire_ability_buttons()
call_deferred("_populate_build_list")
func _process(_delta: float) -> void:
if local_player == null:
local_player = _find_local_player()
if local_player:
var role: int = int(Stats.get_stat(local_player, "role", GameState.ROLE_DAMAGE))
_on_role_changed(local_player, role)
_refresh_vitals()
if build_active:
_update_build_preview()
_update_minimap()
func _is_typing() -> bool:
var f := get_viewport().gui_get_focus_owner()
return f is LineEdit or f is TextEdit
func _unhandled_input(event: InputEvent) -> void:
if _is_typing():
if event.is_action_pressed("pause"):
var f := get_viewport().gui_get_focus_owner()
if f is LineEdit:
(f as LineEdit).release_focus()
_capture_ui(_any_panel_visible())
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("inventory"):
_toggle_panel(inventory_panel)
_refresh_inventory()
elif event.is_action_pressed("crafting"):
_toggle_panel(crafting_panel)
_refresh_crafting()
elif event.is_action_pressed("build_mode"):
_toggle_build_mode()
elif event.is_action_pressed("map"):
_toggle_panel(map_panel)
elif event.is_action_pressed("chat"):
chat_input.grab_focus()
_capture_ui(true)
elif event.is_action_pressed("pause"):
if dialog_panel.visible:
dialog_panel.visible = false
_capture_ui(false)
elif build_active:
_toggle_build_mode()
elif inventory_panel.visible or crafting_panel.visible or map_panel.visible:
inventory_panel.visible = false
crafting_panel.visible = false
map_panel.visible = false
_capture_ui(false)
else:
_toggle_pause()
elif build_active:
if event.is_action_pressed("rotate_build"):
build_rotation = wrapf(build_rotation + PI * 0.5, 0.0, TAU)
elif event.is_action_pressed("ability_1"):
_select_build(0)
elif event.is_action_pressed("ability_2"):
_select_build(1)
elif event.is_action_pressed("ability_3"):
_select_build(2)
elif event.is_action_pressed("ability_4"):
_select_build(3)
elif event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_LEFT:
_try_place()
elif event.button_index == MOUSE_BUTTON_MIDDLE:
_try_remove()
func _wire_ability_buttons() -> void:
for c in ability_box.get_children():
if c is Button:
ability_buttons.append(c)
func _toggle_panel(panel: Control) -> void:
panel.visible = not panel.visible
_capture_ui(_any_panel_visible())
func _any_panel_visible() -> bool:
return inventory_panel.visible or crafting_panel.visible or dialog_panel.visible or pause_panel.visible
func _capture_ui(v: bool) -> void:
if local_player and local_player.has_method("set_ui_capturing"):
local_player.set_ui_capturing(v)
func _find_local_player() -> Node:
for p in get_tree().get_nodes_in_group("player"):
if p.is_multiplayer_authority():
return p
return null
func _refresh_vitals() -> void:
if local_player == null:
return
var hp: float = float(Stats.get_stat(local_player, "health", 0.0))
var max_hp: float = float(Stats.get_stat(local_player, "max_health", 1.0))
_on_health_changed(local_player, hp, max_hp)
var shield: float = float(Stats.get_stat(local_player, "shield", 0.0))
var max_shield: float = float(Stats.get_stat(local_player, "max_shield", 0.0))
_on_shield_changed(local_player, shield, max_shield)
var xp: float = float(Stats.get_stat(local_player, "xp", 0.0))
var to_next: float = float(Stats.get_stat(local_player, "xp_to_next", 50.0))
xp_bar.max_value = to_next
xp_bar.value = xp
level_label.text = "Lv %d" % int(Stats.get_stat(local_player, "level", 1))
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != local_player:
return
hp_bar.max_value = max
hp_bar.value = current
hp_label.text = "%d / %d" % [int(current), int(max)]
func _on_shield_changed(entity: Node, current: float, max: float) -> void:
if entity != local_player:
return
shield_bar.max_value = max if max > 0 else 1
shield_bar.value = current
shield_label.text = "%d / %d" % [int(current), int(max)]
func _on_cooldown_tick(entity: Node, cds: PackedFloat32Array, _max_cds: PackedFloat32Array, gcd: float) -> void:
if entity != local_player:
return
for i in range(min(ability_buttons.size(), cds.size())):
var btn: Button = ability_buttons[i]
if cds[i] > 0.0:
btn.text = "%d\n%.1f" % [i + 1, cds[i]]
btn.disabled = true
elif gcd > 0.0:
btn.text = "%d\nGCD" % (i + 1)
btn.disabled = true
else:
btn.text = "%d" % (i + 1)
btn.disabled = false
func _on_role_changed(player: Node, role: int) -> void:
if player != local_player and player != _find_local_player():
return
if local_player == null:
local_player = player
match role:
GameState.ROLE_TANK:
role_label.text = "T"
role_icon.modulate = Color(0.3, 0.5, 0.95)
GameState.ROLE_DAMAGE:
role_label.text = "D"
role_icon.modulate = Color(0.95, 0.3, 0.3)
GameState.ROLE_HEALER:
role_label.text = "H"
role_icon.modulate = Color(0.4, 0.85, 0.4)
func _on_entity_died(entity: Node) -> void:
if entity != local_player:
return
death_overlay.visible = true
death_label.text = "Respawning..."
func _on_respawned(entity: Node) -> void:
if entity != local_player:
return
death_overlay.visible = false
func _on_wave_started(wave: int) -> void:
wave_label.text = "Wave %d" % wave
func _on_wave_tick(seconds: float) -> void:
var m := int(seconds) / 60
var s := int(seconds) % 60
timer_label.text = "%02d:%02d" % [m, s]
func _on_village_damaged(current: float, max: float) -> void:
village_bar.max_value = max
village_bar.value = current
func _on_village_destroyed() -> void:
game_over_overlay.visible = true
func _on_invasion_started() -> void:
timer_label.modulate = Color(1.0, 0.4, 0.3)
func _on_xp_gained(player: Node, _amount: float) -> void:
if player != local_player:
return
var xp: float = float(Stats.get_stat(player, "xp", 0.0))
var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0))
xp_bar.max_value = to_next
xp_bar.value = xp
func _on_level_up(player: Node, new_level: int) -> void:
if player != local_player:
return
level_label.text = "Lv %d" % new_level
_refresh_vitals()
func _on_dialog_opened(player: Node, npc: Node) -> void:
if player != local_player:
return
dialog_panel.visible = true
dialog_npc_node = npc
dialog_npc.text = npc.profile.display_name
dialog_log.text = "[i]" + npc.profile.greeting + "[/i]\n"
dialog_input.text = ""
dialog_input.grab_focus()
_capture_ui(true)
func _on_dialog_submitted(text: String) -> void:
if dialog_npc_node == null:
return
var dialog_sys: Node = _find_system("DialogSystem")
if dialog_sys == null:
return
dialog_log.append_text("[b]Du:[/b] " + text + "\n[i]...[/i]\n")
dialog_input.text = ""
dialog_sys.ask(dialog_npc_node, local_player, text)
func show_answer(text: String) -> void:
if dialog_npc_node == null:
return
dialog_log.text = dialog_log.text.replace("[i]...[/i]\n", "")
dialog_log.append_text("[b]" + dialog_npc_node.profile.display_name + ":[/b] " + text + "\n")
func _on_inventory_changed(player: Node) -> void:
if player != local_player:
return
_refresh_inventory()
func _refresh_inventory() -> void:
for c in inventory_list.get_children():
c.queue_free()
if local_player == null:
return
var inv_sys: Node = _find_system("InventorySystem")
if inv_sys == null:
return
var inv: Dictionary = inv_sys.get_inventory(local_player)
if inv.is_empty():
var lbl := Label.new()
lbl.text = "(empty)"
inventory_list.add_child(lbl)
return
for k in inv.keys():
var lbl := Label.new()
lbl.text = "%s: %d" % [str(k), inv[k]]
inventory_list.add_child(lbl)
func _refresh_crafting() -> void:
for c in crafting_list.get_children():
c.queue_free()
if local_player == null:
return
var c_sys: Node = _find_system("CraftingSystem")
var inv_sys: Node = _find_system("InventorySystem")
if c_sys == null or inv_sys == null:
return
for r in c_sys.get_recipes():
var btn := Button.new()
var inputs_str: String = ""
for k in r.inputs.keys():
inputs_str += "%s x%d " % [str(k), r.inputs[k]]
btn.text = "%s (%s)" % [r.name, inputs_str.strip_edges()]
btn.disabled = not c_sys.can_craft(local_player, r)
btn.pressed.connect(func(): c_sys.craft(local_player, r.id); _refresh_crafting())
crafting_list.add_child(btn)
func _populate_build_list() -> void:
for c in build_list.get_children():
c.queue_free()
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bps: Array = b_sys.get_blueprints()
for i in range(bps.size()):
var btn := Button.new()
btn.text = "%d %s\n%s x%d" % [i + 1, bps[i].name, str(bps[i].material), bps[i].cost]
btn.toggle_mode = true
btn.button_pressed = (i == 0)
btn.pressed.connect(func(): _select_build(i))
build_list.add_child(btn)
func _select_build(idx: int) -> void:
build_selected = idx
var btns := build_list.get_children()
for i in range(btns.size()):
if btns[i] is Button:
(btns[i] as Button).button_pressed = (i == idx)
if build_preview:
_update_preview_mesh()
func _toggle_build_mode() -> void:
build_active = not build_active
build_panel.visible = build_active
if local_player and local_player.has_method("set_build_mode"):
local_player.set_build_mode(build_active)
if build_active:
_create_build_preview()
elif build_preview:
build_preview.queue_free()
build_preview = null
func _create_build_preview() -> void:
if build_preview:
build_preview.queue_free()
var world: Node = get_tree().current_scene
if world == null:
return
build_preview = MeshInstance3D.new()
build_preview.cast_shadow = MeshInstance3D.SHADOW_CASTING_SETTING_OFF
world.add_child(build_preview)
_update_preview_mesh()
func _update_preview_mesh() -> void:
if build_preview == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
var box := BoxMesh.new()
box.size = bp.size
build_preview.mesh = box
var mat := StandardMaterial3D.new()
var c: Color = bp.color
c.a = 0.5
mat.albedo_color = c
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.flags_unshaded = true
build_preview.material_override = mat
func _update_build_preview() -> void:
if build_preview == null or local_player == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var pos: Vector3 = _ground_under_cursor()
var snapped: Vector3 = b_sys.snap_position(pos)
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
build_preview.global_position = snapped + Vector3(0, bp.size.y * 0.5, 0)
build_preview.rotation.y = build_rotation
func _ground_under_cursor() -> Vector3:
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
if cam == null:
return Vector3.ZERO
var mouse := get_viewport().get_mouse_position()
var from := cam.project_ray_origin(mouse)
var dir := cam.project_ray_normal(mouse)
if abs(dir.y) < 0.001:
return from
var t: float = -from.y / dir.y
if t <= 0:
return from
return from + dir * t
func _try_place() -> void:
if local_player == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bps: Array = b_sys.get_blueprints()
var bp: Dictionary = bps[build_selected]
var pos: Vector3 = _ground_under_cursor()
b_sys.place(local_player, bp.id, pos, build_rotation)
func _try_remove() -> void:
if local_player == null:
return
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
if cam == null:
return
var mouse := get_viewport().get_mouse_position()
var from := cam.project_ray_origin(mouse)
var to := from + cam.project_ray_normal(mouse) * 100.0
var space: PhysicsDirectSpaceState3D = local_player.get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 16
var hit: Dictionary = space.intersect_ray(query)
if hit.is_empty():
return
var node: Node = hit.collider
while node and not node.is_in_group("buildings"):
node = node.get_parent()
if node:
var b_sys: Node = _find_system("BuildingSystem")
if b_sys:
b_sys.remove(local_player, node.get_path())
func _on_chat(_peer_id: int, sender: String, text: String) -> void:
chat_log.append_text("[b]%s:[/b] %s\n" % [sender, text])
func _on_chat_submitted(text: String) -> void:
var c_sys: Node = _find_system("ChatSystem")
if c_sys:
c_sys.send(text)
chat_input.text = ""
chat_input.release_focus()
_capture_ui(_any_panel_visible())
func _toggle_pause() -> void:
pause_panel.visible = not pause_panel.visible
if pause_panel.visible and multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
GameState.set_paused(true)
else:
GameState.set_paused(false)
_capture_ui(_any_panel_visible())
func _on_resume_pressed() -> void:
pause_panel.visible = false
GameState.set_paused(false)
_capture_ui(_any_panel_visible())
func _on_quit_pressed() -> void:
Net.disconnect_net()
GameState.set_paused(false)
GameState.change_scene(GameState.SCENE_MAIN_MENU)
func _on_game_over_restart() -> void:
Net.disconnect_net()
GameState.set_paused(false)
GameState.change_scene(GameState.SCENE_MAIN_MENU)
func _update_minimap() -> void:
_minimap_accum += get_process_delta_time()
if _minimap_accum < 0.20:
return
_minimap_accum = 0.0
minimap_canvas.queue_redraw()
if map_panel.visible:
map_canvas.queue_redraw()
func _find_system(name: String) -> Node:
var n: Node = get_tree().root.get_node_or_null("World/Systems/" + name)
if n == null:
n = get_tree().root.get_node_or_null("Dungeon/Systems/" + name)
return n

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