Compare commits
4 Commits
4b0f82c1de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ad83a96d | |||
|
|
2d4002bd3f | ||
|
|
6d28b04c12 | ||
|
|
087a5ec8cc |
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 `mistral-nemo`
|
||||
- Pro NPC: System-Prompt aus `lore` + `personality`
|
||||
- Fallback: `npc.fallback_text` wenn Ollama nicht erreichbar — Spiel bleibt voll spielbar
|
||||
|
||||
### Bauen
|
||||
- Grid-Snap 1m, 4 Bauteile (Floor, Wall, Door, Roof)
|
||||
- LMB im Build-Mode platziert, MMB entfernt
|
||||
- Material-Verbrauch aus Crafting-Items (alle aus `wood`, das aus `essence` gecrafted wird)
|
||||
- Persistenz via Host (Buildings sind echte Nodes unter `EntityRoot/Buildings`, MultiplayerSpawner repliziert)
|
||||
|
||||
### Wave-Loop
|
||||
1. WaveSystem startet 10-min Timer, spawnt 3 normale + 1 rotes Gate
|
||||
2. Gate stirbt → spawnt Portal an gleicher Position
|
||||
3. Spieler betritt Portal → Dungeon (procedural, 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 `mistral-nemo` erwartet) — Stub-Fallback funktioniert sonst
|
||||
- Save/Load Slots im Hauptmenü (Autoload existiert, UI fehlt)
|
||||
- Multiplayer Late-Join: Building-/Wave-State wird beim spätem Beitritt nicht synchronisiert
|
||||
- Damage Numbers / Hit FX / Particle Effects fehlen (rein Polish)
|
||||
|
||||
@@ -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
|
||||
|
||||
31
autoloads/game_lore.gd
Normal file
31
autoloads/game_lore.gd
Normal file
@@ -0,0 +1,31 @@
|
||||
extends Node
|
||||
|
||||
const WORLD_LORE: String = """Das Land heißt Aerwen. Vor siebzig Wintern öffnete sich der erste Schlund über Aerwen. Niemand weiß, woher sie kommen. Die Alten nennen es 'das Stille Beben' — der Himmel setzte für einen Atemzug aus, dann waren die ersten Risse da.
|
||||
|
||||
Die Schlünde sind Tore zu einer Anderseite — einem Land, das parallel zu unserem zu liegen scheint, aber gehässiger ist. Was dort lebt, atmet durch die Tore in unsere Welt: Knochenwächter, Schattenwölfe, manchmal Schlimmeres. Die meisten Dörfer und Städte sind in den letzten siebzig Jahren gefallen. Aerwen ist heute eine Karte aus leeren Höfen, verfallenen Stadtmauern und einigen wenigen sturen Siedlungen.
|
||||
|
||||
Schimmerthal ist eine dieser Siedlungen. Es liegt in einer flachen Senke, umgeben von brüchigen Wegen und verlassenen Gehöften. Die Bewohner nennen sich selbst 'die Sturen' — die, die nicht weiterziehen wollten oder konnten. Im Zentrum steht die Krumme-Wagen-Taverne, benannt nach einem verkrümmten Handwagen vor der Tür, der dort steht, seit niemand mehr lebt, der sich daran erinnern kann.
|
||||
|
||||
Unter der Taverne liegt ein Anker — ein alter Stein, von dem niemand mehr weiß, wer ihn gesetzt hat. Solange der Anker steht, schwächt er die Schlünde im Umkreis und lässt die Reisenden nicht vollständig sterben.
|
||||
|
||||
Die Schlünde (was Reisende 'Portale' nennen) atmen. Alle paar Stunden öffnet sich irgendwo in der Nähe ein neuer. Wer hindurchschreitet, kommt in einer Höhle der Anderseite heraus. Tötet man die Wächter im Schlund, kollabiert der Riss. Aber er wird ersetzt — irgendwo weiter draußen reißt ein neuer, stärkerer Schlund auf. Die Anderseite gibt nicht auf.
|
||||
|
||||
Etwa einmal pro Atemzug (so nennen die Bewohner die Wellen) reißt der Himmel rot auf — ein 'Tor des Herrn'. Dahinter sitzt eine große Bestie, ein Herr der Anderseite, der einen Schwarm anführt. Wird der Herr besiegt, beruhigt sich die Anderseite kurz. Danach kehrt sie wieder, lauter als vorher — der nächste Atemzug bringt stärkere Wächter. Verstreicht aber die Stunde, ohne dass der Herr fällt, dann erwacht er und führt seinen Schwarm durch den Riss, direkt auf die Taverne zu. Das nennen die Bewohner 'die Invasion'. Fällt die Taverne, fällt der Anker. Niemand weiß, was dann mit dem Land geschieht. Die wenigen, die einmal eine Invasion überlebt haben, sprechen nicht darüber.
|
||||
|
||||
Reisende werden die genannt, die nicht ganz sterben können. Wer einmal den Anker unter der Taverne berührt hat, den lässt der Tod los: ihre Wunden schließen sich, sie tauchen am Anker wieder auf, wenn sie fallen — solange der Anker steht. Es ist kein Segen. Manche im Dorf flüstern, ein Reisender trage ein Stück Anderseite in sich. Nur Reisende können tief in die Schlünde gehen, weil die Anderseite gewöhnliche Lebende zerreißt; einen Reisenden setzt sie nur unangenehm zurück. Reisende kommen selten und gehen oft. Niemand weiß, warum der Anker manche markiert und andere nicht.
|
||||
|
||||
Die wichtigsten Bewohner: Brena, Wirtin der Krumme-Wagen-Taverne, geboren in Schimmerthal, übernahm das Haus von ihrer Großmutter Mara, die als Kind den ersten Schlund aufreißen sah. Halvor, der Schmied, verlor sein rechtes Auge an einen Knochenwächter im fünften Sommer nach den Schlünden. Eyrie, die Murmlerin, ist alt genug, um sich an 'die Zeit davor' zu erinnern, und liest die Risse — sie weiß oft tagelang vorher, wann ein roter Schlund aufbricht. Rolf, der Bauer, bestellt die kleinen Felder östlich des Dorfes und hat zwei Söhne an die Schlünde verloren.
|
||||
|
||||
Geschichten im Dorf: Manche flüstern, die Anderseite seien wir selbst, vor langer Zeit — das gilt als Ammenmärchen. Andere sagen, der Anker sei kein Stein, sondern ein Versprechen, das jemand vor sehr langer Zeit gegeben hat. Eyrie sagt selten etwas dazu; wenn man sie fragt, lächelt sie nur: 'Frag den Anker, wenn du willst.' Es gibt Reisende, die behaupten, in der Anderseite manchmal Häuser zu sehen, die genau so aussehen wie die im Dorf. Halvor schnaubt, wenn er das hört."""
|
||||
|
||||
const DIALECT_HINTS: String = """- Sage 'Schlund' oder 'Riss' oder 'Tor' statt 'Portal'.
|
||||
- Sage 'Atemzug' statt 'Welle'.
|
||||
- Sage 'Reisender' (m) / 'Reisende' (f) statt 'Spieler'.
|
||||
- Sage 'Wächter' oder 'Herr' statt 'Boss'.
|
||||
- Sage 'Anderseite' für das, wohin die Schlünde führen.
|
||||
- Sage 'Anker' für den Stein unter der Taverne.
|
||||
- Im Zweifel ehrlich: 'Das weiß niemand.' statt erfinden."""
|
||||
|
||||
|
||||
func build_npc_context(_profile) -> String:
|
||||
return "WELT:\n%s\n\nSPRACHE:\n%s" % [WORLD_LORE, DIALECT_HINTS]
|
||||
1
autoloads/game_lore.gd.uid
Normal file
1
autoloads/game_lore.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://lyslauvf34ot
|
||||
@@ -1,19 +1,39 @@
|
||||
extends Node
|
||||
|
||||
# Run-Zustand
|
||||
const ROLE_TANK: int = 0
|
||||
const ROLE_DAMAGE: int = 1
|
||||
const ROLE_HEALER: int = 2
|
||||
|
||||
const SCENE_MAIN_MENU: String = "res://scenes/menu/main_menu.tscn"
|
||||
const SCENE_LOBBY: String = "res://scenes/menu/lobby.tscn"
|
||||
const SCENE_WORLD: String = "res://scenes/world/world.tscn"
|
||||
const SCENE_DUNGEON: String = "res://scenes/dungeon/dungeon.tscn"
|
||||
const SCENE_OPTIONS: String = "res://scenes/menu/options_menu.tscn"
|
||||
|
||||
var current_scene: String = SCENE_MAIN_MENU
|
||||
var paused: bool = false
|
||||
var run_seed: int = 0
|
||||
var dungeon_seed: int = 0
|
||||
var dungeon_red: bool = false
|
||||
var current_wave: int = 1
|
||||
var wave_timer_remaining: float = 0.0
|
||||
var run_initialized: bool = false
|
||||
var portal_return_position: Vector3 = Vector3.ZERO
|
||||
|
||||
# Dungeon-Kontext (für XP-Zuordnung nach Clear)
|
||||
var last_dungeon_variant: int = 0
|
||||
|
||||
# Flag für Forced Return (Timer läuft ab während Spieler im Dungeon)
|
||||
var force_return_to_world: bool = false
|
||||
|
||||
func reset() -> void:
|
||||
func reset_run() -> void:
|
||||
run_seed = randi()
|
||||
current_wave = 1
|
||||
wave_timer_remaining = 0.0
|
||||
run_initialized = false
|
||||
last_dungeon_variant = 0
|
||||
force_return_to_world = false
|
||||
dungeon_seed = 0
|
||||
dungeon_red = false
|
||||
paused = false
|
||||
Stats.clear_all()
|
||||
|
||||
func change_scene(path: String) -> void:
|
||||
current_scene = path
|
||||
EventBus.scene_change_requested.emit(path)
|
||||
call_deferred("_do_change_scene", path)
|
||||
|
||||
func _do_change_scene(path: String) -> void:
|
||||
get_tree().change_scene_to_file(path)
|
||||
|
||||
func set_paused(value: bool) -> void:
|
||||
paused = value
|
||||
get_tree().paused = value
|
||||
|
||||
@@ -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
|
||||
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,11 @@ config/icon="res://icon.svg"
|
||||
[autoload]
|
||||
|
||||
EventBus="*res://autoloads/event_bus.gd"
|
||||
Net="*res://autoloads/net.gd"
|
||||
Stats="*res://autoloads/stats.gd"
|
||||
GameState="*res://autoloads/game_state.gd"
|
||||
PlayerData="*res://autoloads/player_stats.gd"
|
||||
EnemyData="*res://autoloads/enemy_stats.gd"
|
||||
BossData="*res://autoloads/boss_stats.gd"
|
||||
PortalData="*res://autoloads/portal_stats.gd"
|
||||
TavernData="*res://autoloads/tavern_stats.gd"
|
||||
SaveLoad="*res://autoloads/save_load.gd"
|
||||
GameLore="*res://autoloads/game_lore.gd"
|
||||
|
||||
[dotnet]
|
||||
|
||||
@@ -51,51 +50,109 @@ move_right={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
jump={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
ability_1={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
ability_2={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
ability_3={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
ability_4={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":52,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
ability_5={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":53,"key_label":0,"unicode":53,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
target_next={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
class_tank={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
class_damage={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
class_healer={
|
||||
"deadzone": 0.2,
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
interact={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
inventory={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
crafting={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
build_mode={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
chat={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":121,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
map={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":109,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
pause={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
rotate_build={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
click_select={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
camera_drag={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
3d_physics/layer_1="World"
|
||||
3d_physics/layer_2="Player"
|
||||
3d_physics/layer_3="Enemy"
|
||||
3d_physics/layer_4="Hitbox"
|
||||
3d_physics/layer_5="Building"
|
||||
|
||||
[physics]
|
||||
|
||||
|
||||
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")]
|
||||
|
||||
132
scenes/dungeon/dungeon_generator.gd
Normal file
132
scenes/dungeon/dungeon_generator.gd
Normal file
@@ -0,0 +1,132 @@
|
||||
extends Node
|
||||
|
||||
const ROOM_HEIGHT: float = 4.0
|
||||
const WALL_THICKNESS: float = 0.4
|
||||
const CORRIDOR_WIDTH: float = 4.0
|
||||
const DOOR_WIDTH: float = 4.5
|
||||
|
||||
var rng: RandomNumberGenerator
|
||||
var rooms: Array = []
|
||||
|
||||
func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dictionary:
|
||||
rng = RandomNumberGenerator.new()
|
||||
rng.seed = seed
|
||||
rooms.clear()
|
||||
var room_count: int = rng.randi_range(5, 8)
|
||||
var pos := Vector3(0, 0, 0)
|
||||
var dir := Vector3(0, 0, -1)
|
||||
for i in range(room_count):
|
||||
var w: float = rng.randf_range(8.0, 14.0)
|
||||
var d: float = rng.randf_range(8.0, 14.0)
|
||||
rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d), "openings": []})
|
||||
if i == room_count - 1:
|
||||
break
|
||||
var corridor_len: float = rng.randf_range(4.0, 8.0)
|
||||
var step: Vector3 = dir * (max(w, d) * 0.5 + corridor_len + 4.0)
|
||||
pos += step
|
||||
if rng.randf() < 0.5:
|
||||
var rotate_left: bool = rng.randf() < 0.5
|
||||
dir = dir.rotated(Vector3.UP, PI * 0.5 * (1 if rotate_left else -1))
|
||||
_compute_openings()
|
||||
_build_geometry(parent)
|
||||
return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos}
|
||||
|
||||
func _compute_openings() -> void:
|
||||
for i in range(rooms.size() - 1):
|
||||
var rel: Vector3 = rooms[i + 1].pos - rooms[i].pos
|
||||
if abs(rel.x) > abs(rel.z):
|
||||
if rel.x > 0:
|
||||
rooms[i].openings.append("east")
|
||||
rooms[i + 1].openings.append("west")
|
||||
else:
|
||||
rooms[i].openings.append("west")
|
||||
rooms[i + 1].openings.append("east")
|
||||
else:
|
||||
if rel.z > 0:
|
||||
rooms[i].openings.append("south")
|
||||
rooms[i + 1].openings.append("north")
|
||||
else:
|
||||
rooms[i].openings.append("north")
|
||||
rooms[i + 1].openings.append("south")
|
||||
|
||||
func _build_geometry(parent: Node3D) -> void:
|
||||
for r in rooms:
|
||||
_build_room(parent, r.pos, r.size, r.openings)
|
||||
for i in range(rooms.size() - 1):
|
||||
_build_corridor(parent, rooms[i].pos, rooms[i + 1].pos)
|
||||
|
||||
func _build_room(parent: Node3D, center: Vector3, size: Vector3, openings: Array) -> void:
|
||||
_add_floor(parent, center, Vector2(size.x, size.z))
|
||||
var hw: float = size.x * 0.5
|
||||
var hd: float = size.z * 0.5
|
||||
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "north", openings, true)
|
||||
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "south", openings, true)
|
||||
_add_wall_with_opening(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "west", openings, false)
|
||||
_add_wall_with_opening(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "east", openings, false)
|
||||
|
||||
func _add_wall_with_opening(parent: Node3D, center: Vector3, size: Vector3, side: String, openings: Array, axis_x: bool) -> void:
|
||||
if not openings.has(side):
|
||||
_add_wall(parent, center, size)
|
||||
return
|
||||
if axis_x:
|
||||
var seg_len: float = (size.x - DOOR_WIDTH) * 0.5
|
||||
if seg_len <= 0.1:
|
||||
return
|
||||
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
|
||||
_add_wall(parent, center + Vector3(-seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
|
||||
_add_wall(parent, center + Vector3(seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
|
||||
else:
|
||||
var seg_len: float = (size.z - DOOR_WIDTH) * 0.5
|
||||
if seg_len <= 0.1:
|
||||
return
|
||||
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
|
||||
_add_wall(parent, center + Vector3(0, 0, -seg_offset), Vector3(size.x, size.y, seg_len))
|
||||
_add_wall(parent, center + Vector3(0, 0, seg_offset), Vector3(size.x, size.y, seg_len))
|
||||
|
||||
func _build_corridor(parent: Node3D, from: Vector3, to: Vector3) -> void:
|
||||
var mid: Vector3 = (from + to) * 0.5
|
||||
var d: float = from.distance_to(to)
|
||||
var dir: Vector3 = (to - from).normalized()
|
||||
_add_floor(parent, Vector3(mid.x, 0, mid.z), Vector2(d, CORRIDOR_WIDTH) if abs(dir.x) > abs(dir.z) else Vector2(CORRIDOR_WIDTH, d))
|
||||
|
||||
func _add_floor(parent: Node3D, center: Vector3, size: Vector2) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 0
|
||||
var mesh := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(size.x, 0.4, size.y)
|
||||
mesh.mesh = box
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.25, 0.22, 0.2)
|
||||
mesh.material_override = mat
|
||||
mesh.position = Vector3(0, -0.2, 0)
|
||||
var col := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = Vector3(size.x, 0.4, size.y)
|
||||
col.shape = shape
|
||||
col.position = Vector3(0, -0.2, 0)
|
||||
body.add_child(mesh)
|
||||
body.add_child(col)
|
||||
body.position = center
|
||||
parent.add_child(body)
|
||||
|
||||
func _add_wall(parent: Node3D, center: Vector3, size: Vector3) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 0
|
||||
var mesh := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = size
|
||||
mesh.mesh = box
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.35, 0.32, 0.28)
|
||||
mesh.material_override = mat
|
||||
var col := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = size
|
||||
col.shape = shape
|
||||
body.add_child(mesh)
|
||||
body.add_child(col)
|
||||
body.position = center
|
||||
parent.add_child(body)
|
||||
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,91 @@
|
||||
extends Node
|
||||
extends Node3D
|
||||
|
||||
const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn")
|
||||
const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn")
|
||||
const PORTAL_SCENE: PackedScene = preload("res://scenes/entities/portal/portal.tscn")
|
||||
const GENERATOR: GDScript = preload("res://scenes/dungeon/dungeon_generator.gd")
|
||||
|
||||
@onready var players_root: Node3D = $EntityRoot/Players
|
||||
@onready var dungeon_root: Node3D = $DungeonGeometry
|
||||
@onready var spawn_system: Node = $Systems/SpawnSystem
|
||||
|
||||
var generator: Node
|
||||
var data: Dictionary
|
||||
|
||||
func _ready() -> void:
|
||||
call_deferred("_scale_dungeon")
|
||||
add_to_group("dungeon")
|
||||
generator = GENERATOR.new()
|
||||
add_child(generator)
|
||||
data = generator.generate(dungeon_root, GameState.dungeon_seed, GameState.current_wave * (5.0 if GameState.dungeon_red else 1.0))
|
||||
EventBus.boss_defeated.connect(_on_boss_defeated)
|
||||
Net.world_ready.connect(_on_world_ready)
|
||||
Net.peer_world_loaded.connect(_on_peer_world_loaded)
|
||||
if Net.is_host():
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
Net.reset_world_ready()
|
||||
Net.mark_world_loaded()
|
||||
|
||||
func _scale_dungeon() -> void:
|
||||
var variant_multiplier: float = 10.0 if GameState.last_dungeon_variant == 1 else 1.0
|
||||
var total_scale: float = PlayerData.level_scale * variant_multiplier
|
||||
var parent: Node = get_parent()
|
||||
for child in parent.get_children():
|
||||
if not child.is_in_group("enemies"):
|
||||
continue
|
||||
if child.is_in_group("boss"):
|
||||
BossData.apply_scale(child, total_scale)
|
||||
else:
|
||||
EnemyData.apply_scale(child, total_scale)
|
||||
func _exit_tree() -> void:
|
||||
if Net.world_ready.is_connected(_on_world_ready):
|
||||
Net.world_ready.disconnect(_on_world_ready)
|
||||
if Net.peer_world_loaded.is_connected(_on_peer_world_loaded):
|
||||
Net.peer_world_loaded.disconnect(_on_peer_world_loaded)
|
||||
|
||||
func _on_world_ready() -> void:
|
||||
if not Net.is_host():
|
||||
return
|
||||
_spawn_player(1)
|
||||
for id in multiplayer.get_peers():
|
||||
_spawn_player(id)
|
||||
_populate_dungeon()
|
||||
|
||||
func _populate_dungeon() -> void:
|
||||
var difficulty: float = GameState.current_wave * (3.0 if GameState.dungeon_red else 1.0)
|
||||
for i in range(data.rooms.size() - 1):
|
||||
var room: Dictionary = data.rooms[i]
|
||||
var n: int = 2 + (1 if GameState.dungeon_red else 0)
|
||||
for j in range(n):
|
||||
var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 1.0, randf_range(-room.size.z * 0.3, room.size.z * 0.3))
|
||||
spawn_system.spawn_enemy_at(room.pos + off, GameState.dungeon_red, difficulty * 0.5)
|
||||
spawn_system.spawn_boss_at(data.boss + Vector3(0, 2.0, 0), difficulty)
|
||||
|
||||
func _spawn_player(peer_id: int) -> void:
|
||||
if players_root.get_node_or_null(str(peer_id)) != null:
|
||||
return
|
||||
var p: CharacterBody3D = PLAYER_SCENE.instantiate()
|
||||
p.name = str(peer_id)
|
||||
players_root.add_child(p, true)
|
||||
p.global_position = data.spawn + Vector3(randf_range(-1, 1), 0, randf_range(-1, 1))
|
||||
|
||||
func _on_peer_connected(id: int) -> void:
|
||||
Net.tell_peer_to_load_scene(id, GameState.SCENE_DUNGEON)
|
||||
|
||||
func _on_peer_world_loaded(peer_id: int) -> void:
|
||||
if not Net.is_host():
|
||||
return
|
||||
_spawn_player(peer_id)
|
||||
|
||||
func _on_peer_disconnected(id: int) -> void:
|
||||
var node := players_root.get_node_or_null(str(id))
|
||||
if node:
|
||||
node.queue_free()
|
||||
|
||||
func _on_boss_defeated(b: Node) -> void:
|
||||
if not Net.is_host():
|
||||
return
|
||||
var portal: StaticBody3D = PORTAL_SCENE.instantiate()
|
||||
portal.is_return = true
|
||||
portal.name = "ReturnPortal"
|
||||
var portals_root: Node3D = $EntityRoot/Portals
|
||||
portals_root.add_child(portal, true)
|
||||
var spawn_pos: Vector3 = (b as Node3D).global_position if b is Node3D else data.boss
|
||||
portal.global_position = spawn_pos + Vector3(0, 1, 0)
|
||||
EventBus.portal_spawned.emit(portal)
|
||||
var t := get_tree().create_timer(3.0)
|
||||
t.timeout.connect(_auto_return.bind(portal))
|
||||
|
||||
func _auto_return(portal: Node) -> void:
|
||||
if is_instance_valid(portal):
|
||||
portal.queue_free()
|
||||
GameState.change_scene(GameState.SCENE_WORLD)
|
||||
|
||||
@@ -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")
|
||||
85
scenes/entities/gate/gate.gd
Normal file
85
scenes/entities/gate/gate.gd
Normal file
@@ -0,0 +1,85 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var stats_resource: GateStats
|
||||
@export var is_red: bool = false
|
||||
|
||||
@onready var mesh: MeshInstance3D = $Mesh
|
||||
@onready var healthbar: MeshInstance3D = $Healthbar
|
||||
@onready var name_label: Label3D = $NameLabel
|
||||
@onready var spawn_point: Node3D = $SpawnPoint
|
||||
|
||||
var spawn_timer: float = 0.0
|
||||
var spawned_count: int = 0
|
||||
var dead: bool = false
|
||||
var attacked: bool = false
|
||||
|
||||
func _enter_tree() -> void:
|
||||
set_multiplayer_authority(1)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("gates")
|
||||
if is_red:
|
||||
add_to_group("red_gate")
|
||||
if stats_resource == null:
|
||||
stats_resource = GateStats.new()
|
||||
if is_red:
|
||||
stats_resource.max_health = 600.0
|
||||
stats_resource.spawn_count = 8
|
||||
else:
|
||||
stats_resource.max_health = 200.0
|
||||
stats_resource.spawn_count = 5
|
||||
stats_resource.is_red = is_red
|
||||
Stats.register(self, stats_resource)
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
if is_red:
|
||||
var mat: StandardMaterial3D = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.95, 0.2, 0.15)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.4, 0.2)
|
||||
mat.emission_energy_multiplier = 0.6
|
||||
mesh.material_override = mat
|
||||
name_label.text = "Red Gate"
|
||||
else:
|
||||
name_label.text = "Gate"
|
||||
set_physics_process(true)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Stats.deregister(self)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if dead:
|
||||
return
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
if not attacked:
|
||||
return
|
||||
spawn_timer = max(0.0, spawn_timer - delta)
|
||||
if spawn_timer <= 0.0 and spawned_count < int(Stats.get_stat(self, "spawn_count", 5)):
|
||||
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
|
||||
if spawn_sys and spawn_sys.has_method("spawn_enemy_at"):
|
||||
spawn_sys.spawn_enemy_at(spawn_point.global_position + Vector3(randf_range(-1.5, 1.5), 0.0, randf_range(-1.5, 1.5)), is_red)
|
||||
spawned_count += 1
|
||||
spawn_timer = float(Stats.get_stat(self, "spawn_interval", 4.0))
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max: float) -> void:
|
||||
if entity != self:
|
||||
return
|
||||
if not attacked and current < max:
|
||||
attacked = true
|
||||
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
|
||||
healthbar.scale.x = max(0.01, ratio * 2.0)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != self or dead:
|
||||
return
|
||||
dead = true
|
||||
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
|
||||
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
|
||||
if spawn_sys and spawn_sys.has_method("spawn_portal_at"):
|
||||
spawn_sys.spawn_portal_at(global_position, is_red)
|
||||
EventBus.gate_destroyed.emit(self)
|
||||
var t := get_tree().create_timer(0.5)
|
||||
t.timeout.connect(func():
|
||||
if is_instance_valid(self):
|
||||
queue_free())
|
||||
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") and not nearby_player.ui_capturing:
|
||||
EventBus.dialog_opened.emit(nearby_player, self)
|
||||
else:
|
||||
prompt.visible = false
|
||||
|
||||
func _on_player_near(body: Node) -> void:
|
||||
if body.is_in_group("player"):
|
||||
if body.is_multiplayer_authority():
|
||||
nearby_player = body
|
||||
|
||||
func _on_player_left(body: Node) -> void:
|
||||
if body == nearby_player:
|
||||
nearby_player = null
|
||||
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")
|
||||
246
scenes/entities/player/player.gd
Normal file
246
scenes/entities/player/player.gd
Normal file
@@ -0,0 +1,246 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
const GRAVITY: float = 18.0
|
||||
const MOUSE_SENS: float = 0.0035
|
||||
|
||||
@export var stats_resource: PlayerStats
|
||||
|
||||
@onready var pivot: Node3D = $Pivot
|
||||
@onready var pitch_pivot: Node3D = $Pivot/PitchPivot
|
||||
@onready var camera: Camera3D = $Pivot/PitchPivot/Camera
|
||||
@onready var mesh_holder: Node3D = $MeshHolder
|
||||
@onready var collision: CollisionShape3D = $Collision
|
||||
@onready var sync: MultiplayerSynchronizer = $Synchronizer
|
||||
@onready var name_label: Label3D = $NameLabel
|
||||
|
||||
var peer_id: int = 1
|
||||
var role: int = GameState.ROLE_DAMAGE
|
||||
var look_dragging: bool = false
|
||||
var current_target: Node = null
|
||||
var dead: bool = false
|
||||
var ui_capturing: bool = false
|
||||
var build_mode: bool = false
|
||||
|
||||
@export var sync_position: Vector3 = Vector3.ZERO
|
||||
@export var sync_velocity: Vector3 = Vector3.ZERO
|
||||
@export var sync_yaw: float = 0.0
|
||||
@export var sync_role: int = GameState.ROLE_DAMAGE
|
||||
|
||||
func _enter_tree() -> void:
|
||||
peer_id = name.to_int()
|
||||
set_multiplayer_authority(peer_id)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
if stats_resource == null:
|
||||
stats_resource = PlayerStats.new()
|
||||
Stats.register(self, stats_resource)
|
||||
Stats.restore_player(peer_id, self)
|
||||
Stats.set_stat(self, "role", role)
|
||||
name_label.text = Net.player_names.get(peer_id, "P%d" % peer_id)
|
||||
EventBus.entity_died.connect(_on_entity_died_clear_target)
|
||||
if is_multiplayer_authority():
|
||||
camera.current = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
camera.current = false
|
||||
_apply_role_visual(role)
|
||||
|
||||
func _on_entity_died_clear_target(entity: Node) -> void:
|
||||
if entity == current_target:
|
||||
current_target = null
|
||||
|
||||
func _set_target(t: Node) -> void:
|
||||
current_target = t
|
||||
EventBus.target_changed.emit(self, t)
|
||||
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and not multiplayer.is_server():
|
||||
_request_target.rpc_id(1, String(t.get_path()) if t else "")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _request_target(path_str: String) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var t: Node = get_node_or_null(NodePath(path_str)) if path_str != "" else null
|
||||
current_target = t
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Stats.cache_player(peer_id, self)
|
||||
Stats.deregister(self)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
if ui_capturing:
|
||||
return
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
look_dragging = event.pressed
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if look_dragging else Input.MOUSE_MODE_VISIBLE
|
||||
elif event is InputEventMouseMotion and look_dragging:
|
||||
pivot.rotate_y(-event.relative.x * MOUSE_SENS)
|
||||
pitch_pivot.rotate_x(-event.relative.y * MOUSE_SENS)
|
||||
pitch_pivot.rotation.x = clamp(pitch_pivot.rotation.x, -1.2, 0.2)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
if dead:
|
||||
return
|
||||
if build_mode:
|
||||
return
|
||||
if ui_capturing:
|
||||
return
|
||||
if event.is_action_pressed("class_tank"):
|
||||
_request_role(GameState.ROLE_TANK)
|
||||
elif event.is_action_pressed("class_damage"):
|
||||
_request_role(GameState.ROLE_DAMAGE)
|
||||
elif event.is_action_pressed("class_healer"):
|
||||
_request_role(GameState.ROLE_HEALER)
|
||||
elif event.is_action_pressed("ability_1"):
|
||||
EventBus.ability_use_requested.emit(self, 0)
|
||||
elif event.is_action_pressed("ability_2"):
|
||||
EventBus.ability_use_requested.emit(self, 1)
|
||||
elif event.is_action_pressed("ability_3"):
|
||||
EventBus.ability_use_requested.emit(self, 2)
|
||||
elif event.is_action_pressed("ability_4"):
|
||||
EventBus.ability_use_requested.emit(self, 3)
|
||||
elif event.is_action_pressed("target_next"):
|
||||
var nt := _cycle_target()
|
||||
_set_target(nt)
|
||||
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed and not look_dragging:
|
||||
var t := _pick_target_under_mouse()
|
||||
if t:
|
||||
_set_target(t)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if is_multiplayer_authority():
|
||||
if not dead:
|
||||
_process_local(delta)
|
||||
sync_position = global_position
|
||||
sync_velocity = velocity
|
||||
sync_yaw = pivot.rotation.y
|
||||
sync_role = role
|
||||
else:
|
||||
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
|
||||
pivot.rotation.y = lerp_angle(pivot.rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
|
||||
if sync_role != role:
|
||||
role = sync_role
|
||||
_apply_role_visual(role)
|
||||
|
||||
func _process_local(delta: float) -> void:
|
||||
if not is_on_floor():
|
||||
velocity.y -= GRAVITY * delta
|
||||
var move_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back") if not ui_capturing else Vector2.ZERO
|
||||
var speed: float = float(Stats.get_stat(self, "speed", 5.0))
|
||||
var basis_y := Basis(Vector3.UP, pivot.rotation.y)
|
||||
var direction := basis_y * Vector3(move_dir.x, 0.0, move_dir.y)
|
||||
if direction.length() > 0.01:
|
||||
velocity.x = direction.x * speed
|
||||
velocity.z = direction.z * speed
|
||||
var look_dir := Vector3(velocity.x, 0.0, velocity.z).normalized()
|
||||
var target_basis := Basis.looking_at(look_dir, Vector3.UP)
|
||||
mesh_holder.basis = mesh_holder.basis.slerp(target_basis, clamp(delta * 12.0, 0.0, 1.0))
|
||||
else:
|
||||
velocity.x = move_toward(velocity.x, 0.0, speed * 6.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, speed * 6.0 * delta)
|
||||
if Input.is_action_just_pressed("jump") and is_on_floor() and not ui_capturing:
|
||||
velocity.y = float(Stats.get_stat(self, "jump_velocity", 4.5))
|
||||
move_and_slide()
|
||||
|
||||
func _request_role(new_role: int) -> void:
|
||||
_set_role.rpc_id(1, new_role)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _set_role(new_role: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
if new_role == role:
|
||||
return
|
||||
role = new_role
|
||||
Stats.set_stat(self, "role", role)
|
||||
_apply_role.rpc(role)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _apply_role(new_role: int) -> void:
|
||||
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
|
||||
return
|
||||
role = new_role
|
||||
if Stats.has(self):
|
||||
Stats.set_stat(self, "role", role)
|
||||
_apply_role_visual(role)
|
||||
EventBus.role_changed.emit(self, role)
|
||||
|
||||
func _apply_role_visual(r: int) -> void:
|
||||
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
|
||||
var mat: StandardMaterial3D = mesh.get_active_material(0).duplicate() if mesh.get_active_material(0) else StandardMaterial3D.new()
|
||||
match r:
|
||||
GameState.ROLE_TANK:
|
||||
mat.albedo_color = Color(0.3, 0.5, 0.95)
|
||||
GameState.ROLE_DAMAGE:
|
||||
mat.albedo_color = Color(0.95, 0.3, 0.3)
|
||||
GameState.ROLE_HEALER:
|
||||
mat.albedo_color = Color(0.4, 0.85, 0.4)
|
||||
mesh.material_override = mat
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func set_dead(value: bool) -> void:
|
||||
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
|
||||
return
|
||||
dead = value
|
||||
visible = not value
|
||||
collision.disabled = value
|
||||
if value:
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func teleport_to(pos: Vector3) -> void:
|
||||
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
|
||||
return
|
||||
global_position = pos
|
||||
sync_position = pos
|
||||
|
||||
func set_ui_capturing(v: bool) -> void:
|
||||
ui_capturing = v
|
||||
|
||||
func set_build_mode(v: bool) -> void:
|
||||
build_mode = v
|
||||
|
||||
func _cycle_target() -> Node:
|
||||
if current_target != null and not is_instance_valid(current_target):
|
||||
current_target = null
|
||||
var candidates: Array = []
|
||||
for n in get_tree().get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(n):
|
||||
candidates.append(n)
|
||||
for n in get_tree().get_nodes_in_group("portals"):
|
||||
if is_instance_valid(n):
|
||||
candidates.append(n)
|
||||
for n in get_tree().get_nodes_in_group("gates"):
|
||||
if is_instance_valid(n):
|
||||
candidates.append(n)
|
||||
if candidates.is_empty():
|
||||
current_target = null
|
||||
return null
|
||||
candidates.sort_custom(func(a, b):
|
||||
return (a as Node3D).global_position.distance_to(global_position) < (b as Node3D).global_position.distance_to(global_position))
|
||||
if current_target == null or not current_target in candidates:
|
||||
current_target = candidates[0]
|
||||
else:
|
||||
var idx: int = candidates.find(current_target)
|
||||
current_target = candidates[(idx + 1) % candidates.size()]
|
||||
return current_target
|
||||
|
||||
func _pick_target_under_mouse() -> Node:
|
||||
var mouse := get_viewport().get_mouse_position()
|
||||
var from := camera.project_ray_origin(mouse)
|
||||
var to := from + camera.project_ray_normal(mouse) * 100.0
|
||||
var space := get_world_3d().direct_space_state
|
||||
var query := PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 0xFFFFFFFF
|
||||
query.exclude = [self]
|
||||
var hit := space.intersect_ray(query)
|
||||
if hit.is_empty():
|
||||
return null
|
||||
var n: Node = hit.collider
|
||||
while n != null and not (n.is_in_group("enemies") or n.is_in_group("portals") or n.is_in_group("gates") or n.is_in_group("npc")):
|
||||
n = n.get_parent()
|
||||
return n
|
||||
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")
|
||||
84
scenes/entities/portal/portal.gd
Normal file
84
scenes/entities/portal/portal.gd
Normal file
@@ -0,0 +1,84 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var stats_resource: PortalStats
|
||||
@export var is_red: bool = false
|
||||
@export var is_return: bool = false
|
||||
|
||||
@onready var mesh: MeshInstance3D = $Mesh
|
||||
@onready var name_label: Label3D = $NameLabel
|
||||
@onready var enter_area: Area3D = $EnterArea
|
||||
|
||||
var triggered: bool = false
|
||||
|
||||
func _enter_tree() -> void:
|
||||
set_multiplayer_authority(1)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("portals")
|
||||
if is_red:
|
||||
add_to_group("red_portal")
|
||||
if stats_resource == null:
|
||||
stats_resource = PortalStats.new()
|
||||
stats_resource.max_health = 1.0
|
||||
stats_resource.is_red = is_red
|
||||
Stats.register(self, stats_resource)
|
||||
enter_area.body_entered.connect(_on_body_entered)
|
||||
if is_return:
|
||||
var mat: StandardMaterial3D = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.85, 0.3)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.8, 0.2)
|
||||
mat.emission_energy_multiplier = 1.2
|
||||
mesh.material_override = mat
|
||||
name_label.text = "Zurück"
|
||||
elif is_red:
|
||||
var mat: StandardMaterial3D = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.95, 0.2, 0.2)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.4, 0.3)
|
||||
mat.emission_energy_multiplier = 1.5
|
||||
mesh.material_override = mat
|
||||
name_label.text = "Red Portal"
|
||||
else:
|
||||
name_label.text = "Portal"
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Stats.deregister(self)
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if not body.is_in_group("player"):
|
||||
return
|
||||
if not body.is_multiplayer_authority():
|
||||
return
|
||||
if triggered:
|
||||
return
|
||||
triggered = true
|
||||
EventBus.portal_entered.emit(self, body)
|
||||
if is_return:
|
||||
_request_return.rpc_id(1)
|
||||
else:
|
||||
_request_enter.rpc_id(1, is_red, global_position)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _request_enter(red: bool, return_pos: Vector3) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
var seed: int = randi()
|
||||
_do_enter.rpc(seed, red, return_pos)
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _do_enter(seed: int, red: bool, return_pos: Vector3) -> void:
|
||||
GameState.dungeon_seed = seed
|
||||
GameState.dungeon_red = red
|
||||
GameState.portal_return_position = return_pos
|
||||
GameState.change_scene(GameState.SCENE_DUNGEON)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _request_return() -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
_do_return.rpc()
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _do_return() -> void:
|
||||
GameState.change_scene(GameState.SCENE_WORLD)
|
||||
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")
|
||||
36
scenes/entities/village/village.gd
Normal file
36
scenes/entities/village/village.gd
Normal file
@@ -0,0 +1,36 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var stats_resource: VillageStats
|
||||
|
||||
@onready var mesh: MeshInstance3D = $Mesh
|
||||
@onready var label: Label3D = $Label
|
||||
@onready var healthbar: MeshInstance3D = $Healthbar
|
||||
|
||||
func _enter_tree() -> void:
|
||||
set_multiplayer_authority(1)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("village")
|
||||
if stats_resource == null:
|
||||
stats_resource = VillageStats.new()
|
||||
stats_resource.max_health = 1000.0
|
||||
Stats.register(self, stats_resource)
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
label.text = "Village"
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Stats.deregister(self)
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max: float) -> void:
|
||||
if entity != self:
|
||||
return
|
||||
EventBus.village_damaged.emit(current, max)
|
||||
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
|
||||
healthbar.scale.x = max(0.01, ratio * 4.0)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != self:
|
||||
return
|
||||
EventBus.village_destroyed.emit()
|
||||
EventBus.game_over.emit()
|
||||
1
scenes/entities/village/village.gd.uid
Normal file
1
scenes/entities/village/village.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://n5yrsrsav4yx
|
||||
47
scenes/entities/village/village.tscn
Normal file
47
scenes/entities/village/village.tscn
Normal file
@@ -0,0 +1,47 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://b0village001"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/entities/village/village.gd" id="1"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape_1"]
|
||||
size = Vector3(6, 4, 6)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_1"]
|
||||
size = Vector3(6, 4, 6)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="VillageMat"]
|
||||
albedo_color = Color(0.55, 0.4, 0.25, 1)
|
||||
|
||||
[sub_resource type="QuadMesh" id="QuadMesh_HB"]
|
||||
size = Vector2(1.0, 0.25)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="HBMat"]
|
||||
shading_mode = 0
|
||||
no_depth_test = true
|
||||
albedo_color = Color(0.4, 0.85, 0.4, 1)
|
||||
|
||||
[node name="Village" type="StaticBody3D"]
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Collision" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
shape = SubResource("BoxShape_1")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
mesh = SubResource("BoxMesh_1")
|
||||
material_override = SubResource("VillageMat")
|
||||
|
||||
[node name="Label" type="Label3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 0)
|
||||
billboard = 1
|
||||
text = "Village"
|
||||
font_size = 28
|
||||
outline_size = 4
|
||||
|
||||
[node name="Healthbar" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.0, 0)
|
||||
mesh = SubResource("QuadMesh_HB")
|
||||
material_override = SubResource("HBMat")
|
||||
gi_mode = 0
|
||||
509
scenes/hud/hud.gd
Normal file
509
scenes/hud/hud.gd
Normal file
@@ -0,0 +1,509 @@
|
||||
extends CanvasLayer
|
||||
|
||||
@onready var hp_bar: ProgressBar = %HpBar
|
||||
@onready var hp_label: Label = %HpLabel
|
||||
@onready var shield_bar: ProgressBar = %ShieldBar
|
||||
@onready var shield_label: Label = %ShieldLabel
|
||||
@onready var xp_bar: ProgressBar = %XpBar
|
||||
@onready var level_label: Label = %LevelLabel
|
||||
@onready var wave_label: Label = %WaveLabel
|
||||
@onready var timer_label: Label = %WaveTimer
|
||||
@onready var village_bar: ProgressBar = %VillageBar
|
||||
@onready var role_icon: Panel = %RoleIcon
|
||||
@onready var role_label: Label = %RoleLabel
|
||||
@onready var ability_box: HBoxContainer = %AbilityBox
|
||||
@onready var death_overlay: Control = %DeathOverlay
|
||||
@onready var death_label: Label = %DeathLabel
|
||||
@onready var chat_log: RichTextLabel = %ChatLog
|
||||
@onready var chat_input: LineEdit = %ChatInput
|
||||
@onready var inventory_panel: Control = %InventoryPanel
|
||||
@onready var inventory_list: VBoxContainer = %InventoryList
|
||||
@onready var crafting_panel: Control = %CraftingPanel
|
||||
@onready var crafting_list: VBoxContainer = %CraftingList
|
||||
@onready var build_panel: Control = %BuildPanel
|
||||
@onready var build_list: HBoxContainer = %BuildList
|
||||
@onready var dialog_panel: Control = %DialogPanel
|
||||
@onready var dialog_npc: Label = %DialogNpc
|
||||
@onready var dialog_log: RichTextLabel = %DialogLog
|
||||
@onready var dialog_input: LineEdit = %DialogInput
|
||||
@onready var map_panel: Control = %MapPanel
|
||||
@onready var map_canvas: Control = %MapCanvas
|
||||
@onready var minimap_canvas: Control = %MinimapCanvas
|
||||
@onready var pause_panel: Control = %PausePanel
|
||||
@onready var game_over_overlay: Control = %GameOverOverlay
|
||||
@onready var ability_buttons: Array = []
|
||||
|
||||
var local_player: Node = null
|
||||
var dialog_npc_node: Node = null
|
||||
var build_selected: int = 0
|
||||
var build_rotation: float = 0.0
|
||||
var build_preview: MeshInstance3D = null
|
||||
var build_active: bool = false
|
||||
var _minimap_accum: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("dialog_ui")
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.shield_changed.connect(_on_shield_changed)
|
||||
EventBus.cooldown_tick.connect(_on_cooldown_tick)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.entity_respawned.connect(_on_respawned)
|
||||
EventBus.wave_started.connect(_on_wave_started)
|
||||
EventBus.wave_timer_tick.connect(_on_wave_tick)
|
||||
EventBus.village_damaged.connect(_on_village_damaged)
|
||||
EventBus.village_destroyed.connect(_on_village_destroyed)
|
||||
EventBus.invasion_started.connect(_on_invasion_started)
|
||||
EventBus.xp_gained.connect(_on_xp_gained)
|
||||
EventBus.level_up.connect(_on_level_up)
|
||||
EventBus.dialog_opened.connect(_on_dialog_opened)
|
||||
EventBus.inventory_changed.connect(_on_inventory_changed)
|
||||
EventBus.chat_message.connect(_on_chat)
|
||||
chat_input.text_submitted.connect(_on_chat_submitted)
|
||||
dialog_input.text_submitted.connect(_on_dialog_submitted)
|
||||
death_overlay.visible = false
|
||||
inventory_panel.visible = false
|
||||
crafting_panel.visible = false
|
||||
build_panel.visible = false
|
||||
dialog_panel.visible = false
|
||||
map_panel.visible = false
|
||||
pause_panel.visible = false
|
||||
game_over_overlay.visible = false
|
||||
set_process(true)
|
||||
_wire_ability_buttons()
|
||||
call_deferred("_populate_build_list")
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if local_player == null:
|
||||
local_player = _find_local_player()
|
||||
if local_player:
|
||||
var role: int = int(Stats.get_stat(local_player, "role", GameState.ROLE_DAMAGE))
|
||||
_on_role_changed(local_player, role)
|
||||
_refresh_vitals()
|
||||
if build_active:
|
||||
_update_build_preview()
|
||||
_update_minimap()
|
||||
|
||||
func _is_typing() -> bool:
|
||||
var f := get_viewport().gui_get_focus_owner()
|
||||
return f is LineEdit or f is TextEdit
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _is_typing():
|
||||
if event.is_action_pressed("pause"):
|
||||
var f := get_viewport().gui_get_focus_owner()
|
||||
if f is LineEdit:
|
||||
(f as LineEdit).release_focus()
|
||||
_capture_ui(_any_panel_visible())
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed("inventory"):
|
||||
_toggle_panel(inventory_panel)
|
||||
_refresh_inventory()
|
||||
elif event.is_action_pressed("crafting"):
|
||||
_toggle_panel(crafting_panel)
|
||||
_refresh_crafting()
|
||||
elif event.is_action_pressed("build_mode"):
|
||||
_toggle_build_mode()
|
||||
elif event.is_action_pressed("map"):
|
||||
_toggle_panel(map_panel)
|
||||
elif event.is_action_pressed("chat"):
|
||||
chat_input.grab_focus()
|
||||
_capture_ui(true)
|
||||
elif event.is_action_pressed("pause"):
|
||||
if dialog_panel.visible:
|
||||
dialog_panel.visible = false
|
||||
_capture_ui(false)
|
||||
elif build_active:
|
||||
_toggle_build_mode()
|
||||
elif inventory_panel.visible or crafting_panel.visible or map_panel.visible:
|
||||
inventory_panel.visible = false
|
||||
crafting_panel.visible = false
|
||||
map_panel.visible = false
|
||||
_capture_ui(false)
|
||||
else:
|
||||
_toggle_pause()
|
||||
elif build_active:
|
||||
if event.is_action_pressed("rotate_build"):
|
||||
build_rotation = wrapf(build_rotation + PI * 0.5, 0.0, TAU)
|
||||
elif event.is_action_pressed("ability_1"):
|
||||
_select_build(0)
|
||||
elif event.is_action_pressed("ability_2"):
|
||||
_select_build(1)
|
||||
elif event.is_action_pressed("ability_3"):
|
||||
_select_build(2)
|
||||
elif event.is_action_pressed("ability_4"):
|
||||
_select_build(3)
|
||||
elif event is InputEventMouseButton and event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_try_place()
|
||||
elif event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
_try_remove()
|
||||
|
||||
func _wire_ability_buttons() -> void:
|
||||
for c in ability_box.get_children():
|
||||
if c is Button:
|
||||
ability_buttons.append(c)
|
||||
|
||||
func _toggle_panel(panel: Control) -> void:
|
||||
panel.visible = not panel.visible
|
||||
_capture_ui(_any_panel_visible())
|
||||
|
||||
func _any_panel_visible() -> bool:
|
||||
return inventory_panel.visible or crafting_panel.visible or dialog_panel.visible or pause_panel.visible
|
||||
|
||||
func _capture_ui(v: bool) -> void:
|
||||
if local_player and local_player.has_method("set_ui_capturing"):
|
||||
local_player.set_ui_capturing(v)
|
||||
|
||||
func _find_local_player() -> Node:
|
||||
for p in get_tree().get_nodes_in_group("player"):
|
||||
if p.is_multiplayer_authority():
|
||||
return p
|
||||
return null
|
||||
|
||||
func _refresh_vitals() -> void:
|
||||
if local_player == null:
|
||||
return
|
||||
var hp: float = float(Stats.get_stat(local_player, "health", 0.0))
|
||||
var max_hp: float = float(Stats.get_stat(local_player, "max_health", 1.0))
|
||||
_on_health_changed(local_player, hp, max_hp)
|
||||
var shield: float = float(Stats.get_stat(local_player, "shield", 0.0))
|
||||
var max_shield: float = float(Stats.get_stat(local_player, "max_shield", 0.0))
|
||||
_on_shield_changed(local_player, shield, max_shield)
|
||||
var xp: float = float(Stats.get_stat(local_player, "xp", 0.0))
|
||||
var to_next: float = float(Stats.get_stat(local_player, "xp_to_next", 50.0))
|
||||
xp_bar.max_value = to_next
|
||||
xp_bar.value = xp
|
||||
level_label.text = "Lv %d" % int(Stats.get_stat(local_player, "level", 1))
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max: float) -> void:
|
||||
if entity != local_player:
|
||||
return
|
||||
hp_bar.max_value = max
|
||||
hp_bar.value = current
|
||||
hp_label.text = "%d / %d" % [int(current), int(max)]
|
||||
|
||||
func _on_shield_changed(entity: Node, current: float, max: float) -> void:
|
||||
if entity != local_player:
|
||||
return
|
||||
shield_bar.max_value = max if max > 0 else 1
|
||||
shield_bar.value = current
|
||||
shield_label.text = "%d / %d" % [int(current), int(max)]
|
||||
|
||||
func _on_cooldown_tick(entity: Node, cds: PackedFloat32Array, _max_cds: PackedFloat32Array, gcd: float) -> void:
|
||||
if entity != local_player:
|
||||
return
|
||||
for i in range(min(ability_buttons.size(), cds.size())):
|
||||
var btn: Button = ability_buttons[i]
|
||||
if cds[i] > 0.0:
|
||||
btn.text = "%d\n%.1f" % [i + 1, cds[i]]
|
||||
btn.disabled = true
|
||||
elif gcd > 0.0:
|
||||
btn.text = "%d\nGCD" % (i + 1)
|
||||
btn.disabled = true
|
||||
else:
|
||||
btn.text = "%d" % (i + 1)
|
||||
btn.disabled = false
|
||||
|
||||
func _on_role_changed(player: Node, role: int) -> void:
|
||||
if player != local_player and player != _find_local_player():
|
||||
return
|
||||
if local_player == null:
|
||||
local_player = player
|
||||
match role:
|
||||
GameState.ROLE_TANK:
|
||||
role_label.text = "T"
|
||||
role_icon.modulate = Color(0.3, 0.5, 0.95)
|
||||
GameState.ROLE_DAMAGE:
|
||||
role_label.text = "D"
|
||||
role_icon.modulate = Color(0.95, 0.3, 0.3)
|
||||
GameState.ROLE_HEALER:
|
||||
role_label.text = "H"
|
||||
role_icon.modulate = Color(0.4, 0.85, 0.4)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != local_player:
|
||||
return
|
||||
death_overlay.visible = true
|
||||
death_label.text = "Respawning..."
|
||||
|
||||
func _on_respawned(entity: Node) -> void:
|
||||
if entity != local_player:
|
||||
return
|
||||
death_overlay.visible = false
|
||||
|
||||
func _on_wave_started(wave: int) -> void:
|
||||
wave_label.text = "Wave %d" % wave
|
||||
|
||||
func _on_wave_tick(seconds: float) -> void:
|
||||
var m := int(seconds) / 60
|
||||
var s := int(seconds) % 60
|
||||
timer_label.text = "%02d:%02d" % [m, s]
|
||||
|
||||
func _on_village_damaged(current: float, max: float) -> void:
|
||||
village_bar.max_value = max
|
||||
village_bar.value = current
|
||||
|
||||
func _on_village_destroyed() -> void:
|
||||
game_over_overlay.visible = true
|
||||
|
||||
func _on_invasion_started() -> void:
|
||||
timer_label.modulate = Color(1.0, 0.4, 0.3)
|
||||
|
||||
func _on_xp_gained(player: Node, _amount: float) -> void:
|
||||
if player != local_player:
|
||||
return
|
||||
var xp: float = float(Stats.get_stat(player, "xp", 0.0))
|
||||
var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0))
|
||||
xp_bar.max_value = to_next
|
||||
xp_bar.value = xp
|
||||
|
||||
func _on_level_up(player: Node, new_level: int) -> void:
|
||||
if player != local_player:
|
||||
return
|
||||
level_label.text = "Lv %d" % new_level
|
||||
_refresh_vitals()
|
||||
|
||||
func _on_dialog_opened(player: Node, npc: Node) -> void:
|
||||
if player != local_player:
|
||||
return
|
||||
dialog_panel.visible = true
|
||||
dialog_npc_node = npc
|
||||
dialog_npc.text = npc.profile.display_name
|
||||
dialog_log.text = "[i]" + npc.profile.greeting + "[/i]\n"
|
||||
dialog_input.text = ""
|
||||
dialog_input.grab_focus()
|
||||
_capture_ui(true)
|
||||
|
||||
func _on_dialog_submitted(text: String) -> void:
|
||||
if dialog_npc_node == null:
|
||||
return
|
||||
var dialog_sys: Node = _find_system("DialogSystem")
|
||||
if dialog_sys == null:
|
||||
return
|
||||
dialog_log.append_text("[b]Du:[/b] " + text + "\n[i]...[/i]\n")
|
||||
dialog_input.text = ""
|
||||
dialog_sys.ask(dialog_npc_node, local_player, text)
|
||||
|
||||
func show_answer(text: String) -> void:
|
||||
if dialog_npc_node == null:
|
||||
return
|
||||
dialog_log.text = dialog_log.text.replace("[i]...[/i]\n", "")
|
||||
dialog_log.append_text("[b]" + dialog_npc_node.profile.display_name + ":[/b] " + text + "\n")
|
||||
|
||||
func _on_inventory_changed(player: Node) -> void:
|
||||
if player != local_player:
|
||||
return
|
||||
_refresh_inventory()
|
||||
|
||||
func _refresh_inventory() -> void:
|
||||
for c in inventory_list.get_children():
|
||||
c.queue_free()
|
||||
if local_player == null:
|
||||
return
|
||||
var inv_sys: Node = _find_system("InventorySystem")
|
||||
if inv_sys == null:
|
||||
return
|
||||
var inv: Dictionary = inv_sys.get_inventory(local_player)
|
||||
if inv.is_empty():
|
||||
var lbl := Label.new()
|
||||
lbl.text = "(empty)"
|
||||
inventory_list.add_child(lbl)
|
||||
return
|
||||
for k in inv.keys():
|
||||
var lbl := Label.new()
|
||||
lbl.text = "%s: %d" % [str(k), inv[k]]
|
||||
inventory_list.add_child(lbl)
|
||||
|
||||
func _refresh_crafting() -> void:
|
||||
for c in crafting_list.get_children():
|
||||
c.queue_free()
|
||||
if local_player == null:
|
||||
return
|
||||
var c_sys: Node = _find_system("CraftingSystem")
|
||||
var inv_sys: Node = _find_system("InventorySystem")
|
||||
if c_sys == null or inv_sys == null:
|
||||
return
|
||||
for r in c_sys.get_recipes():
|
||||
var btn := Button.new()
|
||||
var inputs_str: String = ""
|
||||
for k in r.inputs.keys():
|
||||
inputs_str += "%s x%d " % [str(k), r.inputs[k]]
|
||||
btn.text = "%s (%s)" % [r.name, inputs_str.strip_edges()]
|
||||
btn.disabled = not c_sys.can_craft(local_player, r)
|
||||
btn.pressed.connect(func(): c_sys.craft(local_player, r.id); _refresh_crafting())
|
||||
crafting_list.add_child(btn)
|
||||
|
||||
func _populate_build_list() -> void:
|
||||
for c in build_list.get_children():
|
||||
c.queue_free()
|
||||
var b_sys: Node = _find_system("BuildingSystem")
|
||||
if b_sys == null:
|
||||
return
|
||||
var bps: Array = b_sys.get_blueprints()
|
||||
for i in range(bps.size()):
|
||||
var btn := Button.new()
|
||||
btn.text = "%d %s\n%s x%d" % [i + 1, bps[i].name, str(bps[i].material), bps[i].cost]
|
||||
btn.toggle_mode = true
|
||||
btn.button_pressed = (i == 0)
|
||||
btn.pressed.connect(func(): _select_build(i))
|
||||
build_list.add_child(btn)
|
||||
|
||||
func _select_build(idx: int) -> void:
|
||||
build_selected = idx
|
||||
var btns := build_list.get_children()
|
||||
for i in range(btns.size()):
|
||||
if btns[i] is Button:
|
||||
(btns[i] as Button).button_pressed = (i == idx)
|
||||
if build_preview:
|
||||
_update_preview_mesh()
|
||||
|
||||
func _toggle_build_mode() -> void:
|
||||
build_active = not build_active
|
||||
build_panel.visible = build_active
|
||||
if local_player and local_player.has_method("set_build_mode"):
|
||||
local_player.set_build_mode(build_active)
|
||||
if build_active:
|
||||
_create_build_preview()
|
||||
elif build_preview:
|
||||
build_preview.queue_free()
|
||||
build_preview = null
|
||||
|
||||
func _create_build_preview() -> void:
|
||||
if build_preview:
|
||||
build_preview.queue_free()
|
||||
var world: Node = get_tree().current_scene
|
||||
if world == null:
|
||||
return
|
||||
build_preview = MeshInstance3D.new()
|
||||
build_preview.cast_shadow = MeshInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
world.add_child(build_preview)
|
||||
_update_preview_mesh()
|
||||
|
||||
func _update_preview_mesh() -> void:
|
||||
if build_preview == null:
|
||||
return
|
||||
var b_sys: Node = _find_system("BuildingSystem")
|
||||
if b_sys == null:
|
||||
return
|
||||
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
|
||||
var box := BoxMesh.new()
|
||||
box.size = bp.size
|
||||
build_preview.mesh = box
|
||||
var mat := StandardMaterial3D.new()
|
||||
var c: Color = bp.color
|
||||
c.a = 0.5
|
||||
mat.albedo_color = c
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.flags_unshaded = true
|
||||
build_preview.material_override = mat
|
||||
|
||||
func _update_build_preview() -> void:
|
||||
if build_preview == null or local_player == null:
|
||||
return
|
||||
var b_sys: Node = _find_system("BuildingSystem")
|
||||
if b_sys == null:
|
||||
return
|
||||
var pos: Vector3 = _ground_under_cursor()
|
||||
var snapped: Vector3 = b_sys.snap_position(pos)
|
||||
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
|
||||
build_preview.global_position = snapped + Vector3(0, bp.size.y * 0.5, 0)
|
||||
build_preview.rotation.y = build_rotation
|
||||
|
||||
func _ground_under_cursor() -> Vector3:
|
||||
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
|
||||
if cam == null:
|
||||
return Vector3.ZERO
|
||||
var mouse := get_viewport().get_mouse_position()
|
||||
var from := cam.project_ray_origin(mouse)
|
||||
var dir := cam.project_ray_normal(mouse)
|
||||
if abs(dir.y) < 0.001:
|
||||
return from
|
||||
var t: float = -from.y / dir.y
|
||||
if t <= 0:
|
||||
return from
|
||||
return from + dir * t
|
||||
|
||||
func _try_place() -> void:
|
||||
if local_player == null:
|
||||
return
|
||||
var b_sys: Node = _find_system("BuildingSystem")
|
||||
if b_sys == null:
|
||||
return
|
||||
var bps: Array = b_sys.get_blueprints()
|
||||
var bp: Dictionary = bps[build_selected]
|
||||
var pos: Vector3 = _ground_under_cursor()
|
||||
b_sys.place(local_player, bp.id, pos, build_rotation)
|
||||
|
||||
func _try_remove() -> void:
|
||||
if local_player == null:
|
||||
return
|
||||
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
|
||||
if cam == null:
|
||||
return
|
||||
var mouse := get_viewport().get_mouse_position()
|
||||
var from := cam.project_ray_origin(mouse)
|
||||
var to := from + cam.project_ray_normal(mouse) * 100.0
|
||||
var space: PhysicsDirectSpaceState3D = local_player.get_world_3d().direct_space_state
|
||||
var query := PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 16
|
||||
var hit: Dictionary = space.intersect_ray(query)
|
||||
if hit.is_empty():
|
||||
return
|
||||
var node: Node = hit.collider
|
||||
while node and not node.is_in_group("buildings"):
|
||||
node = node.get_parent()
|
||||
if node:
|
||||
var b_sys: Node = _find_system("BuildingSystem")
|
||||
if b_sys:
|
||||
b_sys.remove(local_player, node.get_path())
|
||||
|
||||
func _on_chat(_peer_id: int, sender: String, text: String) -> void:
|
||||
chat_log.append_text("[b]%s:[/b] %s\n" % [sender, text])
|
||||
|
||||
func _on_chat_submitted(text: String) -> void:
|
||||
var c_sys: Node = _find_system("ChatSystem")
|
||||
if c_sys:
|
||||
c_sys.send(text)
|
||||
chat_input.text = ""
|
||||
chat_input.release_focus()
|
||||
_capture_ui(_any_panel_visible())
|
||||
|
||||
func _toggle_pause() -> void:
|
||||
pause_panel.visible = not pause_panel.visible
|
||||
if pause_panel.visible and multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||
GameState.set_paused(true)
|
||||
else:
|
||||
GameState.set_paused(false)
|
||||
_capture_ui(_any_panel_visible())
|
||||
|
||||
func _on_resume_pressed() -> void:
|
||||
pause_panel.visible = false
|
||||
GameState.set_paused(false)
|
||||
_capture_ui(_any_panel_visible())
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
Net.disconnect_net()
|
||||
GameState.set_paused(false)
|
||||
GameState.change_scene(GameState.SCENE_MAIN_MENU)
|
||||
|
||||
func _on_game_over_restart() -> void:
|
||||
Net.disconnect_net()
|
||||
GameState.set_paused(false)
|
||||
GameState.change_scene(GameState.SCENE_MAIN_MENU)
|
||||
|
||||
func _update_minimap() -> void:
|
||||
_minimap_accum += get_process_delta_time()
|
||||
if _minimap_accum < 0.20:
|
||||
return
|
||||
_minimap_accum = 0.0
|
||||
minimap_canvas.queue_redraw()
|
||||
if map_panel.visible:
|
||||
map_canvas.queue_redraw()
|
||||
|
||||
func _find_system(name: String) -> Node:
|
||||
var n: Node = get_tree().root.get_node_or_null("World/Systems/" + name)
|
||||
if n == null:
|
||||
n = get_tree().root.get_node_or_null("Dungeon/Systems/" + name)
|
||||
return n
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user