refactor
This commit is contained in:
152
CLAUDE.md
152
CLAUDE.md
@@ -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
|
||||
- 1–4: 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 `llama3.2`
|
||||
- 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, 5–8 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 Aufbauen — nicht 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 `llama3.2` 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)
|
||||
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dbr02t7pt4vcn
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bvxn6y15tvidu
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://g7a7xkg1pgb4
|
||||
uid://361x7bdk2j6v
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
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)
|
||||
get_tree().change_scene_to_file(path)
|
||||
|
||||
func set_paused(value: bool) -> void:
|
||||
paused = value
|
||||
get_tree().paused = value
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://c3jq4raqs0onf
|
||||
uid://dettmu50fjtvc
|
||||
|
||||
148
autoloads/net.gd
Normal file
148
autoloads/net.gd
Normal 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
1
autoloads/net.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1k1cufc2skfr
|
||||
@@ -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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://blmuqkl3aro5w
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://doullpjapcsk1
|
||||
46
autoloads/save_load.gd
Normal file
46
autoloads/save_load.gd
Normal 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
|
||||
1
autoloads/save_load.gd.uid
Normal file
1
autoloads/save_load.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bstx6urqlutmq
|
||||
124
autoloads/stats.gd
Normal file
124
autoloads/stats.gd
Normal 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
1
autoloads/stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdrii2l4sefow
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://cet184f878lb8
|
||||
@@ -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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://822h8c1pur1a
|
||||
@@ -1,4 +0,0 @@
|
||||
- player/
|
||||
- player szene
|
||||
- enemy/
|
||||
- enemy szene
|
||||
16
doc/data.md
16
doc/data.md
@@ -1,16 +0,0 @@
|
||||
- core/
|
||||
- persistenz der daten
|
||||
- enthält die wahrheit zum zustand der welt
|
||||
- model
|
||||
- scenes/
|
||||
- verwaltet die darstellung der szene
|
||||
- dürfen berechnen, aber nicht den zustand ändern
|
||||
- logik wird an systeme gesendet und von diesen empfangen
|
||||
- view
|
||||
- systems/
|
||||
- verarbeiten logik von szenen
|
||||
- schnittstelle von szenen und core
|
||||
- dürfen berechnungen durchführen
|
||||
- controller
|
||||
- assets/
|
||||
- objekte für szenen
|
||||
@@ -1,25 +0,0 @@
|
||||
ziel
|
||||
- dorf verteidigen
|
||||
- hohe welle erreichen
|
||||
sieg möglichkeiten
|
||||
- gegner durch angriff besiegen
|
||||
- gegner durch verteidigung besiegen
|
||||
core loop
|
||||
- gegner besiegen
|
||||
- stärker werden
|
||||
- welle überleben
|
||||
meta loop
|
||||
- viele wellen überleben
|
||||
- presige sammeln
|
||||
- pernamente verbesserungen
|
||||
kern
|
||||
- dungeons besiegen um erfahrung und ressourcen zu bekommen
|
||||
- dorf kann durch das bausystem mit ressourcen aus den portalen verbessert werden
|
||||
|
||||
typischer ablauf (angriff)
|
||||
- dorf verteidigen
|
||||
- portale spawnen
|
||||
- protale besiegen -> stärker werden
|
||||
- rotes protal besigen -> welle geschafft
|
||||
- rotes portal bricht nach timer aus -> greift tarverne an
|
||||
- tarverne tod -> game over
|
||||
@@ -1,12 +0,0 @@
|
||||
- WASD bewegung
|
||||
- Leertaste springen mit gravitation
|
||||
|
||||
- gruppe "player"
|
||||
- thirs-person camera
|
||||
- RMB+Hold kamera bewegung, player ausrichtung
|
||||
- leertaste springen
|
||||
- listener
|
||||
- player stirbt
|
||||
- player wird gespawnt
|
||||
- events
|
||||
- fähigkeit verwenden
|
||||
@@ -1,11 +0,0 @@
|
||||
- CharacterBody3D
|
||||
- CollisionShape3D - capsule
|
||||
-
|
||||
|
||||
- init.gd - daten initialisieren
|
||||
- listener.gd - listener registrieren
|
||||
- movement.gd - player bewegung
|
||||
- camera.gd - kamera bewegung
|
||||
- ability.gd -
|
||||
- targeting.gd
|
||||
- role.gd
|
||||
46
export_presets.cfg
Normal file
46
export_presets.cfg
Normal 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
58
plan.md
@@ -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)
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@ 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"
|
||||
|
||||
[dotnet]
|
||||
|
||||
@@ -51,51 +49,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]
|
||||
|
||||
|
||||
20
resources/abilities/ability.gd
Normal file
20
resources/abilities/ability.gd
Normal 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
|
||||
1
resources/abilities/ability.gd.uid
Normal file
1
resources/abilities/ability.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ma4hkxsfia27
|
||||
@@ -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
|
||||
1
resources/abilities/ability_set.gd.uid
Normal file
1
resources/abilities/ability_set.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bn4hk5bimojsi
|
||||
13
resources/buildings/building.gd
Normal file
13
resources/buildings/building.gd
Normal 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
|
||||
1
resources/buildings/building.gd.uid
Normal file
1
resources/buildings/building.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cb61cxd4li4ck
|
||||
15
resources/effects/effect.gd
Normal file
15
resources/effects/effect.gd
Normal 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
|
||||
1
resources/effects/effect.gd.uid
Normal file
1
resources/effects/effect.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvgpetv33u1a8
|
||||
19
resources/effects/element.gd
Normal file
19
resources/effects/element.gd
Normal 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
|
||||
1
resources/effects/element.gd.uid
Normal file
1
resources/effects/element.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duxxvs1ild4we
|
||||
12
resources/items/item.gd
Normal file
12
resources/items/item.gd
Normal 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 = {}
|
||||
1
resources/items/item.gd.uid
Normal file
1
resources/items/item.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cp3sxpqnv31o2
|
||||
9
resources/items/recipe.gd
Normal file
9
resources/items/recipe.gd
Normal 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] = []
|
||||
1
resources/items/recipe.gd.uid
Normal file
1
resources/items/recipe.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ddntj0p320vkq
|
||||
10
resources/npcs/npc_profile.gd
Normal file
10
resources/npcs/npc_profile.gd
Normal 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."
|
||||
1
resources/npcs/npc_profile.gd.uid
Normal file
1
resources/npcs/npc_profile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bcqtqp3jfkfm1
|
||||
8
resources/stats/base_stats.gd
Normal file
8
resources/stats/base_stats.gd
Normal 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
|
||||
1
resources/stats/base_stats.gd.uid
Normal file
1
resources/stats/base_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rotp80banlee
|
||||
@@ -1,2 +1,4 @@
|
||||
extends EnemyStats
|
||||
class_name BossStats
|
||||
extends EnemyStats
|
||||
|
||||
@export var enrage_time: float = 60.0
|
||||
1
resources/stats/boss_stats.gd.uid
Normal file
1
resources/stats/boss_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdyoffdfaor5m
|
||||
2
resources/stats/building_stats.gd
Normal file
2
resources/stats/building_stats.gd
Normal file
@@ -0,0 +1,2 @@
|
||||
class_name BuildingStats
|
||||
extends BaseStats
|
||||
1
resources/stats/building_stats.gd.uid
Normal file
1
resources/stats/building_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://le02tb117uh
|
||||
11
resources/stats/enemy_stats.gd
Normal file
11
resources/stats/enemy_stats.gd
Normal 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
|
||||
1
resources/stats/enemy_stats.gd.uid
Normal file
1
resources/stats/enemy_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bch6113rtsk05
|
||||
7
resources/stats/gate_stats.gd
Normal file
7
resources/stats/gate_stats.gd
Normal 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
|
||||
1
resources/stats/gate_stats.gd.uid
Normal file
1
resources/stats/gate_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cyroet7ce02ew
|
||||
18
resources/stats/player_stats.gd
Normal file
18
resources/stats/player_stats.gd
Normal 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
|
||||
1
resources/stats/player_stats.gd.uid
Normal file
1
resources/stats/player_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c42fif5nbdvur
|
||||
5
resources/stats/portal_stats.gd
Normal file
5
resources/stats/portal_stats.gd
Normal file
@@ -0,0 +1,5 @@
|
||||
class_name PortalStats
|
||||
extends BaseStats
|
||||
|
||||
@export var lifetime: float = 0.0
|
||||
@export var is_red: bool = false
|
||||
1
resources/stats/portal_stats.gd.uid
Normal file
1
resources/stats/portal_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://beg4u508o3lcv
|
||||
2
resources/stats/village_stats.gd
Normal file
2
resources/stats/village_stats.gd
Normal file
@@ -0,0 +1,2 @@
|
||||
class_name VillageStats
|
||||
extends BaseStats
|
||||
1
resources/stats/village_stats.gd.uid
Normal file
1
resources/stats/village_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dpyr5lcc45eub
|
||||
@@ -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")]
|
||||
|
||||
93
scenes/dungeon/dungeon_generator.gd
Normal file
93
scenes/dungeon/dungeon_generator.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
extends Node
|
||||
|
||||
const ROOM_HEIGHT: float = 4.0
|
||||
const WALL_THICKNESS: float = 0.4
|
||||
const CORRIDOR_WIDTH: float = 4.0
|
||||
|
||||
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)})
|
||||
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))
|
||||
_build_geometry(parent)
|
||||
return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos}
|
||||
|
||||
func _build_geometry(parent: Node3D) -> void:
|
||||
for r in rooms:
|
||||
_build_room(parent, r.pos, r.size)
|
||||
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) -> 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(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
|
||||
_add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
|
||||
_add_wall(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
|
||||
_add_wall(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
|
||||
|
||||
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)
|
||||
1
scenes/dungeon/dungeon_generator.gd.uid
Normal file
1
scenes/dungeon/dungeon_generator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1u4odursm3m4
|
||||
@@ -1,16 +1,82 @@
|
||||
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 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), 0.5, 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, 0.5, 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 Net.is_host():
|
||||
var t := get_tree().create_timer(2.0)
|
||||
t.timeout.connect(func():
|
||||
GameState.dungeon_seed = 0
|
||||
_return.rpc())
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _return() -> void:
|
||||
GameState.change_scene(GameState.SCENE_WORLD)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://bfkxrflfn5qx4
|
||||
uid://civwsilci7nlx
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://dlawq281oesnf
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://b07aajhufqvb3
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://bh2uuuvl30y0x
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://vy6hyqok0p8b
|
||||
36
scenes/entities/building/building.gd
Normal file
36
scenes/entities/building/building.gd
Normal 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
|
||||
1
scenes/entities/building/building.gd.uid
Normal file
1
scenes/entities/building/building.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cq63wtaqo3ebt
|
||||
22
scenes/entities/building/building.tscn
Normal file
22
scenes/entities/building/building.tscn
Normal 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")
|
||||
170
scenes/entities/enemy/enemy.gd
Normal file
170
scenes/entities/enemy/enemy.gd
Normal 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
|
||||
1
scenes/entities/enemy/enemy.gd.uid
Normal file
1
scenes/entities/enemy/enemy.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://t28sckpnef15
|
||||
76
scenes/entities/enemy/enemy.tscn
Normal file
76
scenes/entities/enemy/enemy.tscn
Normal 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")
|
||||
80
scenes/entities/gate/gate.gd
Normal file
80
scenes/entities/gate/gate.gd
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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())
|
||||
1
scenes/entities/gate/gate.gd.uid
Normal file
1
scenes/entities/gate/gate.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yrf2o25mrihx
|
||||
53
scenes/entities/gate/gate.tscn
Normal file
53
scenes/entities/gate/gate.tscn
Normal 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)
|
||||
39
scenes/entities/loot/loot_drop.gd
Normal file
39
scenes/entities/loot/loot_drop.gd
Normal 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()
|
||||
1
scenes/entities/loot/loot_drop.gd.uid
Normal file
1
scenes/entities/loot/loot_drop.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://begk6dfrigj12
|
||||
35
scenes/entities/loot/loot_drop.tscn
Normal file
35
scenes/entities/loot/loot_drop.tscn
Normal 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")
|
||||
52
scenes/entities/npc/npc.gd
Normal file
52
scenes/entities/npc/npc.gd
Normal 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"):
|
||||
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
|
||||
1
scenes/entities/npc/npc.gd.uid
Normal file
1
scenes/entities/npc/npc.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chul3jysytie2
|
||||
53
scenes/entities/npc/npc.tscn
Normal file
53
scenes/entities/npc/npc.tscn
Normal 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")
|
||||
242
scenes/entities/player/player.gd
Normal file
242
scenes/entities/player/player.gd
Normal file
@@ -0,0 +1,242 @@
|
||||
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.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.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 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
|
||||
1
scenes/entities/player/player.gd.uid
Normal file
1
scenes/entities/player/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w43lhoat7ccq
|
||||
58
scenes/entities/player/player.tscn
Normal file
58
scenes/entities/player/player.tscn
Normal 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")
|
||||
62
scenes/entities/portal/portal.gd
Normal file
62
scenes/entities/portal/portal.gd
Normal file
@@ -0,0 +1,62 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var stats_resource: PortalStats
|
||||
@export var is_red: 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_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)
|
||||
_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)
|
||||
1
scenes/entities/portal/portal.gd.uid
Normal file
1
scenes/entities/portal/portal.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cny0k5s884uco
|
||||
49
scenes/entities/portal/portal.tscn
Normal file
49
scenes/entities/portal/portal.tscn
Normal 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")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user