This commit is contained in:
Marek Lenczewski
2026-04-04 00:00:15 +02:00
parent 3488856b91
commit f1d34ebf1d
104 changed files with 1912 additions and 1789 deletions

View File

@@ -27,14 +27,15 @@ Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare n
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
- `scenes/` — Darstellung + Input - `scenes/` — Darstellung + Input
- `player/` — Spieler + player_stats + role/ (Rollen + Abilities) - `player/` — Spieler + player_stats + role/ (Rollen + Abilities)
- `enemy/` — Gegner + Boss + enemy_stats + boss_stats - `enemy/` — Gegner + enemy_stats + boss/ (Boss + boss_stats)
- `portal/` — Portal + Gate + portal_stats - `portal/` — Portal + Gate + portal_stats
- `dungeon/` — Dungeon + dungeon_manager - `dungeon/` — Dungeon + dungeon_manager
- `hud/` — HUD - `hud/` — HUD
- `world/` — Hauptszene + portal_spawner - `world/` — Hauptszene + portal_spawner
- `healthbar.gd` — Shared Component - `effect_icon_factory.gd` — Shared Utility (Effekt-Icons)
- `healthbar*.gd` — Healthbar-Komponenten (health, shield, status, effects)
- `systems/` — Spiellogik - `systems/` — Spiellogik
- 11 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element) - 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) - `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten)
- `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config - `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config
- `autoloads/` — Globaler Zustand - `autoloads/` — Globaler Zustand
@@ -44,13 +45,6 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
## Planungsdokument ## 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. `plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand.
## Design-Dokumente
Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design-Docs:
- `story.md` — Gameplay-Loop, Szenarien, Steuerung
- `idden.md` — Alle Ideen, Kernphilosophie, Technik
- `Szenarien.md` — Ressourcenanfragen, Dungeons, Gemeinschaft, Endgame
- `Level 1.md` bis `Level 3.md` — Systeme nach Priorität
## Core Loop ## Core Loop
1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum) 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%) 2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%)

64
autoloads/boss_stats.gd Normal file
View File

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

View File

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

64
autoloads/enemy_stats.gd Normal file
View File

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

View File

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

View File

@@ -1,8 +1,12 @@
extends Node extends Node
# Intentionen (Input → System) # Intentionen (Input → System)
signal ability_use_requested(player, ability_index) signal ability_use(player, ability_index)
signal role_change_requested(player, role)
signal target_requested(player, target)
signal enemy_detected(enemy, player) signal enemy_detected(enemy, player)
signal enemy_lost(enemy, player)
signal portal_entered(portal, player)
# Kampf # Kampf
signal attack_executed(attacker, position, direction, damage) signal attack_executed(attacker, position, direction, damage)
@@ -29,7 +33,6 @@ signal buff_changed(entity, stat, value)
# Gegner # Gegner
signal enemy_engaged(enemy, target) signal enemy_engaged(enemy, target)
signal enemy_lost(enemy, player)
# Portal # Portal
signal portal_spawn(portal, enemies) signal portal_spawn(portal, enemies)

View File

@@ -1,20 +0,0 @@
extends Node
var player_role: int = 1
var portal_position: Vector3 = Vector3.ZERO
var returning_from_dungeon := false
var dungeon_cleared := false
func save_player(player: Node) -> void:
var role: Node = player.get_node("Role")
player_role = role.current_role
func restore_player(player: Node) -> void:
var role: Node = player.get_node("Role")
role.set_role(player_role)
func clear() -> void:
Stats.clear_player_cache()
portal_position = Vector3.ZERO
returning_from_dungeon = false
dungeon_cleared = false

View File

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

145
autoloads/player_stats.gd Normal file
View File

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

View File

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

45
autoloads/portal_stats.gd Normal file
View File

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

View File

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

View File

@@ -1,53 +0,0 @@
extends Node
var entities: Dictionary = {}
var player_cache: Dictionary = {}
func register(entity: Node, base: BaseStats) -> void:
if entity.is_in_group("player") and not player_cache.is_empty():
entities[entity] = player_cache.duplicate()
entities[entity]["base_stats"] = base
player_cache.clear()
else:
entities[entity] = {
"base_stats": base,
"health": base.max_health,
"max_health": base.max_health,
"health_regen": base.health_regen,
"shield": base.max_shield,
"max_shield": base.max_shield,
"shield_regen_delay": base.shield_regen_delay,
"shield_regen_time": base.shield_regen_time,
"shield_regen_timer": 0.0,
"alive": true,
"buff_damage": 1.0,
"buff_heal": 1.0,
"buff_shield": 1.0,
}
func deregister(entity: Node) -> void:
if entity.is_in_group("player") and entity in entities:
player_cache = entities[entity].duplicate()
entities.erase(entity)
func clear_player_cache() -> void:
player_cache.clear()
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) -> BaseStats:
if entity in entities:
return entities[entity]["base_stats"]
return null
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false

View File

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

26
comminication.md Normal file
View File

@@ -0,0 +1,26 @@
# Player
- movement -> position_changed(player, vector) -> movement_system
- attack -> attack_used(player, target) -> attack_system
- ability -> ability_used(player, target) -> ability_system
- role -> changed_role(player, role) -> role_system
- targeting -> target_requested(player, target) -> targeting_system
- camera -> no events
# Enemy
- detection -> player_detected(enemy, player) -> aggro_system
- detection -> player_lost(enemy, player) -> aggro_system
# HUD
- on_health_changed(player, value) -> health
- on_shield_changed(player, value) -> shield
# Systems
- movement_system <- on_position_changed(player, vector)
- attack_system <- attack_used(player, target)
- ability_system <- ability_used(player, target)
- damage_calculator_system ->
- health_system -> damage_dealt(player, damage) -> PlayerStats()
# Autoloads
- PlayerStats -> - health -> on_health_changed(player, value)
- PlayerStats -> - health -> on_health_changed(player, value)

63
features.md Normal file
View File

@@ -0,0 +1,63 @@
# Player
- Movement - WASD laufen, Leertaste springen
- Bewegung korrigiert Camera in Laufrichtung
- Camera - 3rd Person, hinter Spieler, bewegung RMB/LMB + ziehen
- Beim ziehen der Kamera bewegt sich der zeiger nicht
- Targeting - Automatisch den nächsten Gegner bei Kampfbegin anvisieren, im Sichtradius priorisieren
- Targetwechsel per Tab oder Mausklick auf den Gegner
- Automatisches Target nur, wenn kein Target anvisiert wird
- Autoattack - Automatische angriffe im Kampf auf das Target
- Tank (1dmg + 1shield, 3m Range, GCD)
- Damage (2dmg, 20m Range, GCD)
- Heal (1 heal auf lowsten player, 20m Range, GCD)
- Role - Wechsel per ALT + 1 (Tank), + 2 (Damage), + 3 (Healer)
- Stats werden beim Wechsel nicht zurückgesetzt (HP, Schild, CDs, ...) sondern der größere genommen
- Ability - Wechsel per 1 (Single), 2 (AOE), 3 (Utility), 4 (Ult)
- Passive (P) ist eine Aura
- Single (2s CD), AOE (5s CD), Utitlty (15s CD), Ult (30s)
- Basis-Tank:
- Single (10dmg+10shield, 3m range)
- AOE (5dmg 10m radius, 5shield for each enemy hitted, 3m range)
- Utility (no function)
- Ult (no function)
- Passive (+50% Shield, 50m range)
- Basis-Damage:
- Single (20dmg, 20m range)
- AOE (10dmg 5m radius, 20m range)
- Utility (no function)
- Ult (no function)
- Passive (+50% damage, 50m range)
- Basis-Heal:
- Single (20heal, 20m range)
- AOE (10heal 20m radius, 20m range)
- Utility (no function)
- Ult (no function)
- Passive (+50% heal, 50m range)
- Basisstats - Health (100), Shield (100)
- Death - Respawn after 3s at tarvern with full hp/shield
- Shield - Used before Health, 10%/s regen after 5s no dmg get
- Aggro - Tank 4x
- Gegner Aggro verteilt sich zu 0.5x an Gegner im 10m range
- Nach 5s ohne schaden bekommen oder verursachen endet der kampf
- Gegner Aggro sinkt um 2^(s) für jede Sekunden aus dem Kampf
# Enemy
- KI-States:
- Chase - Gegner hat Aggro und läuft auf den Spieler
- Attack - Gegner greift Spieler an
- Return - Gegner hat Aggro verloren, zurück zum Spawn mit 10%/s HP reg, kein aggrozuwachs bis beim spawn
# Protal
- Spawn:
# Dungeon
-
# HUD
- Health - Links-oben, grün, hp/max text
- Shield - Links-oben unter Health, blau, shield/max text
- Health und Shield schwarzer Rand zusammen
- Abilities - Unten Zentral, 4 Rechteck mit 1-4 text, 1 Kreis mit P text
- Role - Unten zentral links von Abilites, Kreis mit T/S/H
- Role und Abilites schwarzer Rand
- Role bestimmen Hintergrund von Abilites / Role, T - blau, S - Rot, H - Grün
- Aura, Buff, Debuff - rechts oben, in der Reihenfolge, schwarzer Rand
- Hintergrund Aura - blau, Buff - Gründ, Debuff - Rot
# Welt
- Globaler Cooldown 0.5s

View File

@@ -1,46 +0,0 @@
Hauptsysteme
- Welt
- Spieler
- Gegner
- NPC
- Dungeons
- HUD
- Fähigkeiten
- Klassen
Zusatzsysteme
- Berufe
- Quest
- Items
- Inventar
- Equipment
Nebensysteme
- Audio
- Licht
- Transport
- Tageszeit
- Wetter
- Presitige
- Einstellungen
- Achivments
- Events
- Tutorial
- Speichersystem
- Game Over
- Admin-Tools
- Narrative
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Chat
- Gruppe
- Handel
- Offline-Fortschritt
---
Nicht
- Gilden - Jeder ist in Teil der Community
- Housing - Fokus auf Tarverne und Zentrum
- Kosmetik - Aussehen durch Crafting und Fortschritt bestimmt

View File

@@ -1,60 +0,0 @@
Hauptsysteme
- Welt
- Spieler
- Gegner
- NPC
- Dungeons
- HUD
- KI
- Kampf
- Fähigkeiten
- Eigenschaften
- Klassen
- Berufe
- Quest
- Items
- Inventar
- Equipment
- Loot
- Fortschritt
Nebensysteme
- Audio
- Licht
- Transport
- Tageszeit
- Wetter
- Presitige
- Einstellungen
- Achivments
- Events
- Tutorial
- Speichersystem
- Game Over
- Admin-Tools
- Narrative
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Chat
- Gruppe
- Handel
- Offline-Fortschritt
---
Nicht
- Gilden - Jeder ist in Teil der Community
- Housing - Fokus auf Tarverne und Zentrum
- Kosmetik - Aussehen durch Crafting und Fortschritt bestimmt
Alex / Sanni
Billi / Kai
Lea / Christopf
Dösi
Nina / Igor
Basti
Vincent
Bennet

View File

@@ -1,110 +0,0 @@
Hauptsysteme
- Welt
- Boden, Hügel, Gebäude, Natur
- Spieler
- Mesh, Textur, Steuerung, Animation
- Verwandlung - Kampf und Normale Gestalt
- Gestaltanpassung - Primärattribut > Rest
- Elemente verändern Aussehen
- Gegner
- Mesh, Textur, Animation
- Skalierung - Passend sich dem Level an
- NPC
- Mesh, Textur, Animation
- Fallback - Ein NPC je mögliche Spieler Position
- KI
- Gegner KI
- NPC KI
- Kampf
- Schadensberechnung
- Angriff
- Bei Tod respawn in der Stadt
- AFK - Unbesiegbar, Castet automatisch Fähigkeiten mit halber Stärke, wenn in Gruppe
- Dungeons
- Gebietgenerierung
- Instanzierung - Eine Instanz, tauchen zufällig auf
- Break - Dungon öffnet nach einer Weile, Elite Monster starten eine Invasion
- Invasion - Kein Respawn
- Loot - 50% Gruppenleistung und 50% Eigenleistung
- HUD
- Leben, Schild, Name, Stufe
- Fähigkeiten
- Minimap
- Anpassung
- Fähigkeiten
- Arten - Single, AOE, Utility, Ulti, Passive
- Elemente manipulieren Fähigkeiten
- Eigenschaften
- Leben, Schild
- Attribute
- Automatische verbesserung mit Aktionen
- Klassen
- Klassenauswahl
- Arten - Verteidigung, Nahkampf, Fernkampf, Heilung, Unterstützung
- Wechsel - Rollenwechsel geht immer
- Berufe
- Übersicht, Annehmen, Wechseln
- Arten - Abendteurer, Quester, Schmied, Baumeister, Forscher, Archologe
- Abendteurer - Monster bekämpfen
- Forschung - Unbekannte Objekte identifizieren
- Crafting - Objekte erstellen oder verbessern, Ressourcen abbauen
- Bauen - Objekte erstellen oder verbessern, Ressourcen abbauen
- Entdecken - Unbekannte Objekte finden
- Quest
- Öffnen, Annehmen, Abbrechen, Schließen
- Erstellen - Quester kann eigene Quests erstellen
- Schwarzes Brett - Aktuelle Aufgaben von NPCs oder Spielern
- Items
- Mesh, Textur
- Inventar
- Items aufnehmen und ablegen
- Größe unendlich
- Equipment
- Loot
- Fortschritt
- Aktionen geben Erfahrung
- Globaler Fortschritt - Jede Aktion gibt einen Teil an jeden Spieler
- Globale Freischaltung - Jede Freischaltung gilt für alle Spieler
Nebensysteme
- Audio
- Musik, Sounds
- Licht
- Lokal, Global
- Transport
- Extremschnell auf Wegen
- Tageszeit
- Tag, Abend, Nacht
- Wetter
- Sonne, Regen, Schnee
- Presitige
- Einstellungen
- Achivments
- Spielfortschritt, Entdecken, Maxlevel
- Events
- Tutorial
- Keins, Gameplay selbsterklärend gestalten
- Speichersystem
- Speichern, Laden
- Game Over
- Tarverne wird zerstört
- Belohnung - Teil vom Fortschritt gibt Erharung zum Prestige
- Admin-Tools
- Narrative
- Geschichte durch Welt / NPC / Berichte erzählen
- Wirtschaft
- Karte
Mehrspieler
- Multiplayer
- Client, Server, Lokalität
- Chat
- Lokaler Chat
- Globaler Chat
- Gruppen Chat
- Gruppe
- Spieler in der nähe bilden automatisch eine Gruppe
- Gruppensuche
- Handel
- Einfaches Handelfenster
- Offline-Fortschritt

View File

@@ -1,26 +0,0 @@
Baum
- Welt
- Spieler
Welt - Node3D
- Boden - MeshInstance3D
- PlaneMesh 10mx10m
- StandardMaterial3D mit NoiseTexture2D als Albedo
- NoiseTexture2D: FastNoiseLite, Gradient dunkelgrün zu hellgrün, seamless
- UV-Skalierung für Wiederholung
- DirectionalLight3D — Sonnenlicht mit Schatten, 45° Winkel
- Camera3D — 30° Winkel nach unten geneigt
Spieler - CharacterBody3D
- Kollision - CollisionShape3D mit CapsuleShape3D
- Mesh - MeshInstance3D mit CapsuleMesh
- Kamera
- CameraPivot - Node3D, Position am Kopf des Spielers
- Camera3D - Position hinter/über dem Spieler
- Stript - LMB Kamera und Laufrichtung bewegen, RMB Kamera bewegen
- Steuerung
- Skript - WASD Bewegung relativ zur Kamera, Leertaste Springen
Gegner - CharacterBody3D
- Kollision - CollisionShape3D
- Mesh - MeshInstance3D mit CapsuleMesh, Rot

View File

@@ -1,38 +0,0 @@
# Szenarien
Ressourcenanfragen
- Heiler will Zauber verbessern -> Ressourcen fehlen -> Anfrage erstellen
- Quester sieht die Anfrage > Quester nimmt sie an und erstellt eine Quest
- Abenteurer sieht eine Quest -> Nimmt Quest an und besorgt die Ressource
- Abendteuert gibt Ressource an Quester -> Bekommt Erfahrung, Gold und Ruf
- Quester gibt Ressource -> Bekommt Erfahrung, Gold und Ruf
- Heiler bekommt Ressource -> Zahl Geld -> Verbessert die Fähigkieten
Dungeons
- Portal taucht auf -> Wird eingestufft und auf der Karte angezeigt
- Abedteurer können das Portal betreten und es leeren -> Erfahrung und Ressourcen
- Miner können Ressourcen abbauen -> Verkaufen für Erfahrung und Gold
- Archologen können Portal untersuchen -> Forschung verbessern
- Dungen wird nicht besiegt -> Dungeon bricht und greift das Dorf an
- Wird das Hauptgebäude zerstört -> Kein Respawn Möglich -> Dorf stirbt
Gemeinschaft
- Bewohner erledigen aufgaben -> Ort entwickelt sich und wird stärker
- Je Stärker der Ort, desto schwer sind die Aufgaben
- Stärkere Spieler fokussieren sich auf schwere Aufgabe
- Anfänger auf einfache Aufgaben
- Gemeinschaft bekommt Vorteile durch jeden Spieler
- Wird ein Spieler besser dann wird die Geminschaft besser
Endgame
- Gemeinschaft ist fertig ausgebaut
- Durchschnitt der Spieler hat ein bestimmtes Niveau erreicht
- "Spiel wird einfach"
- Letzte Invasion -> Portale tauchen auf -> Greifen dorf an -> Spieler verteidigen sich
- Kein Respawn -> Am Ende alle Gegner oder Spiele sind besiegt
- Sieg für Spieler -> Prestige Fähigkeiten
- Sieg für Gegner -> Dorf stirbt -> Game Over
Loop
- Dorf beitretten -> Gemeinschaft entsteht -> Gemeinschaft wächst
- Gemeinschaft überlebt Invasion -> Prestige und Reset
- Neues Dorf --> ...

View File

@@ -1,70 +0,0 @@
## Alle Ideen Übersicht
**Kernphilosophie**
- Spiel gegen die drei Urängste: soziale Zurückweisung, Überforderung, Versagen
- Angst nicht entfernen, sondern durch positive Gefühle ersetzen
- Zielgruppe: einsame Menschen, denen eine Brücke zur echten Welt gebaut wird
- Stilles Designprinzip, nicht offen als "Anti-Einsamkeits-Spiel" kommuniziert
**Lokalität**
- Spieler werden nach geographischer Nähe im gleichen Layer gruppiert
- Dynamischer Radius: bei geringer Dichte wird er vergrößert bis global
- Globaler Server als Opt-out für Spieler, die Anonymität wollen
- Ziel: Online-Freundschaften können natürlich zu echten werden
- Treffen ist selbstorganisiert, kein eingebautes Feature
**Spieldesign**
- Jede Aktion hilft automatisch anderen, auch Solospielen
- Keine formellen Gruppen jeder in einem Bereich ist automatisch Teil des Geschehens
- Dungeons spawnen als Invasionen auf der Karte mit verschiedenen Schwierigkeiten
- Boss skaliert dynamisch mit Spieleranzahl
- Versagen eines Einzelnen schadet nie der Gruppe
- Kein Blame, kein DPS-Meter, kein Kick-System
**Dynamische Gemeinschaft**
- Spieler können Abenteurer oder "NPCs" sein Schmied, Questgeber, Händler, Gastwirt
- Stadt-Spieler und Abenteurer sind voneinander abhängig
- Offline-Automatismen: Läden und Quests laufen weiter wenn der Spieler offline ist
- Die Welt fühlt sich immer belebt an
**Progressionssystem**
- Berufungs-Zähler wie in Plunderer jede Tätigkeit hat einen sichtbaren Rang
- Individuelle Ränge stärken die gesamte Gemeinschaft
- Regionale Gesamtstärke schaltet bessere Inhalte frei
- Kein Neid, da jeder Aufstieg allen hilft
- Natürliche Spezialisierung durch offene Nischen
**Technik**
- Godot 4 als Client
- Dedizierter Server mit API-Architektur
- Client sendet Intentionen, Server validiert
- Offline-Automatismen laufen serverseitig
- Anti-Cheat by Design: selbst Cheater helfen der Gemeinschaft
**Monetarisierung**
- Free-to-Play mit kosmetischen Items
- Cosmetics werden wertvoller durch Lokalität und soziale Bindung
Hier die neuen Punkte aus unserer Diskussion:
- Jede Spielerrolle hat ein NPC-Pendant als Fallback das Spiel funktioniert solo, wird aber mit Spielern besser
- Invasionen skalieren mit Anzahl und Stärke der Spieler
- Solo-Invasion ist die einfachste Stufe, stärkste Invasionen nur auf öffentlichen Servern
- Soziale Angst abbauen, aber Verlustangst als Motivator nutzen
- Gemeinsame Vorbereitung auf die Endinvasion ist das Endgame-Ziel
- Prestige-Verbesserungen erweitern den Spielstil statt nur Werte zu erhöhen
- Prestige schaltet einzigartige Fähigkeiten frei, darunter Wiederbelebung
- Normales Spiel hat normalen Respawn
- Endinvasion: kein normaler Respawn, nur Prestige-Spieler können wiederbeleben
- Prestige-Spieler werden im Finale zum Sicherheitsnetz der Gemeinschaft
- Kernloop: Bedeutung aufbauen → alles riskieren → Triumph → Prestige-Reset → neu beginnen mit erweiterten Fähigkeiten
- Anfangs Godot 2D für den Prototyp für Core Loops testing
- Später Godot 3D mit Blender Objekten mit wenigen Polygonen
- Kleine Karte Anfangs

View File

@@ -1,58 +0,0 @@
Shortstory
- Dorf in der Mitte
- Portale spawnen außen mit Monstern und einem Boss
- Nach 1 Tag bricht das Portal und Monter greifen Dorf an
- Spieler besigen Monster und bekommen Essenz
- Teil der gewonnen Essenz wird an alle Spieler verteilt
- Spieler können Kampfrollen annehmen Tank, Schaden, Heiler
- Spieler haben 5 Fähigkeiten: Single, AOE, Utility, Ult, Passive
- Fähigkeiten unterscheiden sich je nach Kampfrolle
- Spieler können Elemente verwenden, sie verändern die Fähigkeiten
- Spieler können Berufe annehmen: Quester, Crafter, Forscher, Archologe, Handwerker
- Berufe verbessern den Kampf und Verteidigung
- Nach 7 Tagen kommt eine Invasion mit starken Gegnern
- Beim Sieg bekommt man Presitigeessenz, die man für dauerhafte Verbesserungen verwenden kann
- Wird das Dorf zerstört, dann heißt es Game Over
Allgemein
- Eine Welt startet mit einem Dorfplatz und einem Helfer.
- Der Helfer soll neuen Spielern helfen.
- Die Welt besteht aus einem Bereich.
- In dem Berech erscheinen Portale, diese sind auf der Karte zu sehen.
- In dem Portal sind Monster und ein Boss.
- Nach einem Tag bricht das Portal und die Monster greifen das Dorf an.
- Stirbt der Helfer, dann heißt es Game Over.
- Stirbt ein Spieler, dann wird er am Dorfplatz wiederbelebt.
- Spieler können Monster für Loot und Erfahrung bekämpfen.
- Jeder Spieler in der Welt bekommt einen kleinen Teil vom Loot.
- Mit Loot kann man Inhalte freischalten.
- Inhalte sind sowasw wie Beruffreischaltung, Fähigkeitenverbesserungen, Elementverbesserungen, Items
- Berufe sind Abenteurer - Monster bekämpfen, Archologe - Neues finden, Forscher - Neues identifizieren, Handwerker - Gebäude erstellen und verbessern, Crafter - Items erstellen und verbessern, Quester - Aufgaben verwalten
- Elemente wie Feuer, Eis, Finternis, Gift, ... verändern wie Fähigkeiten funktionieren
- Wenn man Monster bekämpft bekommt man automatisch Attribute je nach der Role - Tank, Schaden, Heiler
- Der Beruf hat die Stufen F,E,D,C,B,A,S,S+, je höher die Stufe, desto mehr kann man machen
- Die Attribute, Elemente und der höchste Berufsrang verändern das Aussehen
- Nach 7 Tagen kommt eine Invasion mit starken Gegnern, dabei gibt es kein Respawn, wird der Helfer getötet dann heißt es Game Over, man bekommt % der geschafften Invasion als Prestige gutgeschrieben
- Besiegt man die Invasion, dann endet das Spiel mit 100% und einem sicheren Prestigepunkt
Szenarien
- Spiel start
- Menü - Einzelspiel, Koop, Online, Einstellungen, Quit
- Einzelspiel - Neues Spiel, Laden
- Neues Spiel - Name eingeben, dann startet das Spiel
- Neues Spieler erscheit
- HUD links-oben - Leben, Schild, Rang
- HUD mitte-unten - Kampfhaltung, Fähigkeiten
- HUD rechts-oben - Minimap
- Fähigkeiten - Single, AOE, Utility, Ult, Passive (1 bis 5)
- Kapfhaltung - Tank, Schaden, Heiler, Neutral (ALT+1 bis ALT+4)
- Laufen mit WASD
- Leertaste springen
- Linke Maustaste Kamera bewegen und Laufrichtung mitziehn
- Rechte Maustaste nur Kamera bewegen
- Welt - Boden, Hügel, Natur
- Weg - Bewegung +300%
- Dorf - Nur Tarverne

58
plan.md
View File

@@ -4,21 +4,27 @@
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
``` ```
scenes/ — Darstellung + Input scenes/ — Darstellung + Input
healthbar.gd — Shared Component effect_icon_factory.gd — Shared Utility (statisch, Effekt-Icon-Erstellung)
healthbar.gd — Health-Anzeige + Viewport-Setup
healthbar_shield.gd — Shield-Anzeige
healthbar_status.gd — Target-Border + Aggro-Farbwechsel
healthbar_effects.gd — Effekt-Icons auf Healthbar
player/ — Spieler + player_stats player/ — Spieler + player_stats
role/ — Rollenwechsel + Ability/AbilitySet-Klassen role/ — Rollenwechsel + Ability/AbilitySet-Klassen
damage/ — set.tres + abilities/ damage/ — set.tres + abilities/
tank/ — set.tres + abilities/ tank/ — set.tres + abilities/
healer/ — set.tres + abilities/ healer/ — set.tres + abilities/
enemy/ — Gegner + Boss + enemy_stats + boss_stats enemy/ — Gegner + enemy_stats
boss/ — Boss + boss_stats
portal/ — Portal + Gate + portal_stats portal/ — Portal + Gate + portal_stats
dungeon/ — Dungeon + dungeon_manager dungeon/ — Dungeon + dungeon_manager
hud/ — HUD hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects)
world/ — Hauptszene + portal_spawner world/ — Hauptszene + portal_spawner
systems/ — Spiellogik systems/ — Spiellogik
aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config
effect.gd — Effect Resource (Buff/Debuff/Aura Daten) effect.gd — Effect Resource (Buff/Debuff/Aura Daten)
10× *_system.gd — health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element 11× *_system.gd — health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, buff_calc
aura_system.gd — Aura-Propagierung (Child von EffectSystem)
autoloads/ — Globaler Zustand autoloads/ — Globaler Zustand
event_bus.gd event_bus.gd
game_state.gd game_state.gd
@@ -27,7 +33,7 @@ autoloads/ — Globaler Zustand
## Szenenbaum ## Szenenbaum
- Welt - Welt
- Systems (11 Systeme als Child-Nodes) - Systems (12 Systeme als Child-Nodes)
- Taverne - Taverne
- Player - Player
- Portale (dynamisch) - Portale (dynamisch)
@@ -136,20 +142,26 @@ autoloads/ — Globaler Zustand
- Listener: entity_died - Listener: entity_died
- Event: respawn_tick, player_respawned - Event: respawn_tick, player_respawned
### AbilitySystem (ability_system.gd) ### AbilitySystem (ability_system.gd)
- Ability-Ausführung (Single, AOE, Utility, Ult) + Auto-Attack in _process - Ability-Ausführung (Single, AOE, Utility, Ult)
- Listener: ability_use_requested - Listener: ability_use_requested
- Event: attack_executed, damage_requested, heal_requested - Event: attack_executed, damage_requested, heal_requested
### AutoAttackSystem (auto_attack_system.gd)
- Auto-Attack-Logik, läuft jeden Frame in _process
- Liest CombatState.in_combat + Targeting.current_target
- Event: damage_requested, heal_requested
### EffectSystem (effect_system.gd) ### EffectSystem (effect_system.gd)
- Verwaltet Buffs, Debuffs und Auras auf Entities - Verwaltet Buffs, Debuffs und Auras auf Entities
- Effect Resource (effect.gd): effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration, is_multiplier, aura_radius, tick_interval, element - Effect Resource (effect.gd): effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration, is_multiplier, aura_radius, tick_interval, element
- State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer) - State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer)
- Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt - Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt
- Passive-Abilities werden als AURA-Effekte erstellt (role_changed → permanente Auras) - Passive-Abilities werden als AURA-Effekte erstellt (role_changed → permanente Auras)
- Aura-Propagierung: Auras mit aura_radius > 0 geben allen Spielern im Radius einen temporären Buff (0.5s, refreshed jedes Frame). Verlässt man den Radius → Buff sofort weg - _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks
- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks, Aura-Propagierung
- _recalc_stat_buffs: Multiplier-Effekte aggregieren → buff_damage/heal/shield in Stats
- Listener: role_changed, entity_died, effect_requested - Listener: role_changed, entity_died, effect_requested
- Event: buff_changed, shield_changed, effect_applied, effect_expired - Event: effect_applied, effect_expired
- Children:
- AuraSystem (aura_system.gd) — Aura-Propagierung im Radius, Buff-Refresh
- BuffCalcSystem (buff_calc_system.gd) — Multiplier aggregieren → Stats + Shield updaten
- Event: buff_changed, shield_changed
### ElementSystem (element_system.gd) ### ElementSystem (element_system.gd)
- Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus - Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus
- Element Enum: NONE, FIRE (erweiterbar) - Element Enum: NONE, FIRE (erweiterbar)
@@ -164,8 +176,6 @@ autoloads/ — Globaler Zustand
- Cooldown-Tracking, GCD, AA-Timer per Entity - Cooldown-Tracking, GCD, AA-Timer per Entity
- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem - register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem
- Event: cooldown_tick - Event: cooldown_tick
### DamageSystem (damage_system.gd)
- Reserviert für spätere Schadensberechnung (aktuell leer)
### AggroSystem (systems/aggro/) ### AggroSystem (systems/aggro/)
- Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres) - Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres)
- aggro_system.gd — Parent, Config halten, Children verdrahten - aggro_system.gd — Parent, Config halten, Children verdrahten
@@ -209,8 +219,9 @@ autoloads/ — Globaler Zustand
- Camera3D - Camera3D
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats - Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
- Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested - Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested
- CombatState (Node, combat_state.gd) — in_combat-Tracking, Combat-Timer
- Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready) - Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready)
- Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested - Targeting (Node, targeting.gd) — Klick/TAB/Auto-Target, emittiert target_changed
- player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn - player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung - camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung
@@ -223,7 +234,10 @@ autoloads/ — Globaler Zustand
- DetectionArea (Area3D, emittiert enemy_detected) - DetectionArea (Area3D, emittiert enemy_detected)
- NavigationAgent3D - NavigationAgent3D
- EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle - EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle
- Healthbar (Sprite3D + SubViewport, healthbar.gd) — liest HP/Shield von Stats, zeigt Effekt-Icons - 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
- enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal - enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal
- Aggro-Regeln (Werte in AggroConfig Resource): - Aggro-Regeln (Werte in AggroConfig Resource):
- Aufbau: - Aufbau:
@@ -254,6 +268,9 @@ autoloads/ — Globaler Zustand
- HitArea (Area3D) - HitArea (Area3D)
- DetectionArea (Area3D, Auto-Targeting bei Betreten) - DetectionArea (Area3D, Auto-Targeting bei Betreten)
- Healthbar (Sprite3D + SubViewport, healthbar.gd) - Healthbar (Sprite3D + SubViewport, healthbar.gd)
- HealthbarShield (Node, healthbar_shield.gd)
- HealthbarStatus (Node, healthbar_status.gd)
- HealthbarEffects (Node, healthbar_effects.gd)
- portal.gd — Registriert bei Stats mit PortalStats Resource - portal.gd — Registriert bei Stats mit PortalStats Resource
- Spawnt Gegner bei HP-Schwellen (→ SpawnSystem) - Spawnt Gegner bei HP-Schwellen (→ SpawnSystem)
@@ -266,7 +283,7 @@ autoloads/ — Globaler Zustand
## Dungeon (dungeon/) ## Dungeon (dungeon/)
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht) - dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
- Systems (alle 11 Systeme, temporär bis Welt parallel läuft) - Systems (alle 12 Systeme, temporär bis Welt parallel läuft)
- NavigationRegion3D - NavigationRegion3D
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch) - Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
- Spieler (Instanz von player.tscn) - Spieler (Instanz von player.tscn)
@@ -278,13 +295,15 @@ autoloads/ — Globaler Zustand
- Eigene Systems bis Welt parallel läuft (geplant: Reparenting) - Eigene Systems bis Welt parallel läuft (geplant: Reparenting)
## HUD (hud/) ## HUD (hud/)
- hud.tscn — CanvasLayer - hud.tscn — CanvasLayer (kein Root-Skript)
- HealthBar (ProgressBar, Label) - HealthBar (ProgressBar, Label)
- ShieldBar (ProgressBar, Label) - ShieldBar (ProgressBar, Label)
- EffectContainer (HBoxContainer, programmatisch, unter ShieldBar)
- RespawnTimer (Label, Countdown bei Tod) - RespawnTimer (Label, Countdown bei Tod)
- AbilityBar (HBoxContainer, RoleIcon + Abilities 1-4 + Passive) - AbilityBar (HBoxContainer, RoleIcon + Abilities 1-4 + Passive)
- hud.gd — Reagiert auf Events, liest Werte von Stats, zeigt Effekt-Icons - HudVitals (Node, hud_vitals.gd) — HP/Shield-Bars
- HudRespawn (Node, hud_respawn.gd) — Respawn-Timer
- HudAbilities (Node, hud_abilities.gd) — Ability-Bar + Cooldowns + Rollen-Icon
- HudEffects (Node, hud_effects.gd) — Effekt-Icons (nutzt EffectIconFactory)
# Abilities (Werte) # Abilities (Werte)
- Schadens-Klasse: - Schadens-Klasse:
@@ -308,3 +327,6 @@ autoloads/ — Globaler Zustand
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD - 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
- 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD - 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD
- 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD) - 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD)
---

72
plan2.md Normal file
View File

@@ -0,0 +1,72 @@
# Kommunikation
## Datenfluss
```
Szene → Event → System → Autoload → Event → Szene/HUD
```
## Autoloads
- PlayerStats — Spieler-Daten (HP, Shield, Rolle, Target, Cooldowns, Position)
- EnemyStats — Enemy-Daten pro Node (HP, Shield, State, Target)
- BossStats — Boss-Daten pro Node (wie Enemy, andere Basiswerte)
- PortalStats — Portal-Daten pro Node (HP, Thresholds)
- EventBus — Signale
## Player
Scripts: init, movement, targeting, role, camera, ability
Events out:
- ability_use(player, ability_index)
- role_change_requested(player, role)
- target_requested(player, target)
## Enemy
Scripts: init, detection
Events out:
- enemy_detected(enemy, player)
- enemy_lost(enemy, player)
## Boss
Scripts: init, detection (erbt von Enemy)
Events out: wie Enemy
## Portal
Scripts: init
Events out:
- portal_entered(portal, player)
## Gate
Scripts: gate
Liest: PlayerStats (Szenenwechsel)
## Dungeon
Scripts: dungeon_manager
Hört: BossStats.died
## HUD
Scripts: hud_vitals, hud_respawn, hud_abilities, hud_effects
Hört: PlayerStats Events (health_changed, shield_changed, died, respawned, role_changed, cooldown_tick)
## Healthbar
Scripts: healthbar, healthbar_shield, healthbar_status, healthbar_effects
Hört: health_changed(entity), shield_changed(entity), target_changed, effect_applied/expired
## Systems → wer hört was, wer schreibt wohin
| System | Hört | Schreibt |
|--------|------|----------|
| role_system | role_change_requested | PlayerStats |
| ability_system | ability_use | → damage_system/heal_system |
| damage_system | (von ability, attack, ai, debuff) | PlayerStats/EnemyStats/BossStats/PortalStats |
| heal_system | (von ability, buff) | PlayerStats/EnemyStats/BossStats |
| attack_system | _process | → damage_system/heal_system |
| shield_system | _process | PlayerStats/EnemyStats/BossStats |
| cooldown_system | _process | PlayerStats |
| respawn_system | PlayerStats.died | PlayerStats |
| targeting_system | target_requested, entity_died | PlayerStats |
| aggro_system | damage_dealt, enemy_detected/lost, entity_died | EnemyStats/BossStats |
| aura_system | _process | → buff_system |
| buff_system | effect_requested | PlayerStats |
| debuff_system | effect_requested | → damage_system |
| spawn_system | health_changed (Portal) | EnemyStats |
| ai_system | _process | EnemyStats/BossStats, → damage_system |
| element_system | element_damage_dealt | → debuff_system |

View File

@@ -18,8 +18,10 @@ config/icon="res://icon.svg"
[autoload] [autoload]
EventBus="*res://autoloads/event_bus.gd" EventBus="*res://autoloads/event_bus.gd"
Stats="*res://autoloads/stats/stats.gd" PlayerData="*res://autoloads/player_stats.gd"
GameState="*res://autoloads/game_state.gd" EnemyData="*res://autoloads/enemy_stats.gd"
BossData="*res://autoloads/boss_stats.gd"
PortalData="*res://autoloads/portal_stats.gd"
[dotnet] [dotnet]

View File

@@ -3,23 +3,34 @@
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"] [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/hud/hud.tscn" id="hud"]
[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"] [ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"]
[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"] [ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"]
[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"] [ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"]
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"] [ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] [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/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/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_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_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_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/aggro/aggro_events.gd" id="aggro_events"]
[ext_resource type="Script" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"] [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/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/spawn_system.gd" id="spawn_system"]
[ext_resource type="Script" path="res://systems/effect_system.gd" id="effect_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/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"]
[sub_resource type="NavigationMesh" id="NavigationMesh_1"] [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) 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)
@@ -58,18 +69,33 @@ size = Vector3(0.5, 3, 90)
[node name="HealthSystem" type="Node" parent="Systems"] [node name="HealthSystem" type="Node" parent="Systems"]
script = ExtResource("health_system") 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")
[node name="ShieldSystem" type="Node" parent="Systems"] [node name="ShieldSystem" type="Node" parent="Systems"]
script = ExtResource("shield_system") script = ExtResource("shield_system")
[node name="DamageSystem" type="Node" parent="Systems"] [node name="RoleSystem" type="Node" parent="Systems"]
script = ExtResource("damage_system") 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"] [node name="AbilitySystem" type="Node" parent="Systems"]
script = ExtResource("ability_system") script = ExtResource("ability_system")
[node name="AttackSystem" type="Node" parent="Systems"]
script = ExtResource("attack_system")
[node name="CooldownSystem" type="Node" parent="Systems"] [node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("cooldown_system") script = ExtResource("cooldown_system")
[node name="TargetingSystem" type="Node" parent="Systems"]
script = ExtResource("targeting_system")
[node name="AggroSystem" type="Node" parent="Systems"] [node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("aggro_system") script = ExtResource("aggro_system")
@@ -82,8 +108,8 @@ script = ExtResource("aggro_decay")
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"] [node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_events") script = ExtResource("aggro_events")
[node name="EnemyAISystem" type="Node" parent="Systems"] [node name="AISystem" type="Node" parent="Systems"]
script = ExtResource("enemy_ai_system") script = ExtResource("ai_system")
[node name="RespawnSystem" type="Node" parent="Systems"] [node name="RespawnSystem" type="Node" parent="Systems"]
script = ExtResource("respawn_system") script = ExtResource("respawn_system")
@@ -91,12 +117,24 @@ script = ExtResource("respawn_system")
[node name="SpawnSystem" type="Node" parent="Systems"] [node name="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("spawn_system") script = ExtResource("spawn_system")
[node name="EffectSystem" type="Node" parent="Systems"] [node name="AuraSystem" type="Node" parent="Systems"]
script = ExtResource("effect_system") script = ExtResource("aura_system")
[node name="BuffSystem" type="Node" parent="Systems"]
script = ExtResource("buff_system")
[node name="DebuffSystem" type="Node" parent="Systems"]
script = ExtResource("debuff_system")
[node name="ElementSystem" type="Node" parent="Systems"] [node name="ElementSystem" type="Node" parent="Systems"]
script = ExtResource("element_system") script = ExtResource("element_system")
[node name="HudSystem" type="Node" parent="Systems"]
script = ExtResource("hud_system")
[node name="NameplateSystem" type="Node" parent="Systems"]
script = ExtResource("nameplate_system")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_1") navigation_mesh = SubResource("NavigationMesh_1")
@@ -203,13 +241,14 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60)
[node name="Enemy4d" parent="." instance=ExtResource("enemy")] [node name="Enemy4d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
[node name="Boss" parent="." instance=ExtResource("boss")] [node name="Boss" parent="." groups=["boss"] instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75) 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")] [node name="ExitGate" parent="." instance=ExtResource("gate")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
target_scene = "res://scenes/world/world.tscn" target_scene = "res://scenes/world/world.tscn"
is_exit = true is_exit = true
[node name="DungeonManager" type="Node" parent="."] [node name="DungeonSystem" type="Node" parent="Systems"]
script = ExtResource("dungeon_manager") script = ExtResource("dungeon_system")

View File

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

View File

@@ -1,5 +0,0 @@
extends "res://scenes/enemy/enemy.gd"
func _ready() -> void:
super._ready()
add_to_group("boss")

View File

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

View File

@@ -1,108 +0,0 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://scenes/enemy/boss.gd" id="1"]
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="4"]
[ext_resource type="Script" path="res://scenes/enemy/enemy_movement.gd" id="5"]
[ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="8"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.6
height = 2.0
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
radius = 0.6
height = 2.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
bg_color = Color(0.3, 0.1, 0.1, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
bg_color = Color(0.2, 0.8, 0.2, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.6, 0.1, 0.6, 1)
[sub_resource type="SphereMesh" id="SphereMesh_1"]
radius = 0.75
height = 1.5
material = SubResource("StandardMaterial3D_1")
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 8.0
[node name="Boss" type="CharacterBody3D"]
script = ExtResource("1")
stats = ExtResource("8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0)
mesh = SubResource("SphereMesh_1")
[node name="HitArea" type="Area3D" parent="."]
collision_layer = 4
collision_mask = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
shape = SubResource("CapsuleShape3D_2")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
[node name="EnemyMovement" type="Node" parent="."]
script = ExtResource("5")
[node name="DetectionArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 1
monitoring = true
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"]
shape = SubResource("SphereShape3D_1")
[node name="Healthbar" type="Sprite3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0)
billboard = 1
pixel_size = 0.01
script = ExtResource("4")
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
transparent_bg = true
size = Vector2i(104, 29)
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"]
offset_right = 104.0
offset_bottom = 29.0
color = Color(1, 0.9, 0.2, 1)
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"]
offset_left = 2.0
offset_top = 2.0
offset_right = 102.0
offset_bottom = 12.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
max_value = 500.0
value = 500.0
show_percentage = false
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport"]
offset_left = 2.0
offset_top = 15.0
offset_right = 102.0
offset_bottom = 27.0
theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill")
max_value = 100.0
value = 100.0
show_percentage = false
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"]

View File

@@ -1 +1 @@
uid://bio01w2gd5e7q uid://dlawq281oesnf

11
scenes/enemy/detection.gd Normal file
View File

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

View File

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

View File

@@ -1,37 +0,0 @@
extends CharacterBody3D
enum State { IDLE, CHASE, ATTACK, RETURN }
@export var stats: BaseStats
var state: int = State.IDLE
var target: Node3D = null
var spawn_position: Vector3
var portal: Node3D = null
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
func _ready() -> void:
spawn_position = global_position
add_to_group("enemies")
Stats.register(self, stats)
EventBus.entity_died.connect(_on_entity_died)
func _exit_tree() -> void:
Stats.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity == self:
queue_free()
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_detected.emit(self, body)
func _on_detection_area_body_exited(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_lost.emit(self, body)

View File

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

View File

@@ -1,18 +1,26 @@
[gd_scene load_steps=6 format=3] [gd_scene format=3 uid="uid://db8pa55ev4l4a"]
[ext_resource type="Script" path="res://scenes/enemy/enemy.gd" id="1"] [ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"]
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="4"] [ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"]
[ext_resource type="Script" path="res://scenes/enemy/enemy_movement.gd" id="5"] [ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"]
[ext_resource type="Resource" path="res://scenes/enemy/enemy_stats.tres" id="8"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.4 radius = 0.4
height = 1.5 height = 1.5
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.8, 0.1, 0.1, 1)
[sub_resource type="SphereMesh" id="SphereMesh_1"]
material = SubResource("StandardMaterial3D_1")
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
radius = 0.4 radius = 0.4
height = 1.5 height = 1.5
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 10.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
bg_color = Color(0.3, 0.1, 0.1, 1) bg_color = Color(0.3, 0.1, 0.1, 1)
@@ -25,74 +33,58 @@ bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1) bg_color = Color(0.2, 0.5, 0.9, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] [node name="Enemy" type="CharacterBody3D" unique_id=1724620529]
albedo_color = Color(0.8, 0.1, 0.1, 1)
[sub_resource type="SphereMesh" id="SphereMesh_1"]
radius = 0.5
height = 1.0
material = SubResource("StandardMaterial3D_1")
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 10.0
[node name="Enemy" type="CharacterBody3D"]
script = ExtResource("1") script = ExtResource("1")
stats = ExtResource("8") stats = ExtResource("8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038]
shape = SubResource("CapsuleShape3D_1") shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."] [node name="Mesh" type="MeshInstance3D" parent="." unique_id=1598094615]
mesh = SubResource("SphereMesh_1") mesh = SubResource("SphereMesh_1")
[node name="HitArea" type="Area3D" parent="."] [node name="HitArea" type="Area3D" parent="." unique_id=893463784]
collision_layer = 4 collision_layer = 4
collision_mask = 0 collision_mask = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"] [node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962]
shape = SubResource("CapsuleShape3D_2") shape = SubResource("CapsuleShape3D_2")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] [node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945]
[node name="EnemyMovement" type="Node" parent="."] [node name="Detection" type="Node" parent="." unique_id=534240144]
script = ExtResource("5") script = ExtResource("2")
[node name="DetectionArea" type="Area3D" parent="."] [node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598]
collision_layer = 0 collision_layer = 0
collision_mask = 1
monitoring = true
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"] [node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347]
shape = SubResource("SphereShape3D_1") shape = SubResource("SphereShape3D_1")
[node name="Healthbar" type="Sprite3D" parent="."] [node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
billboard = 1 billboard = 1
pixel_size = 0.01
script = ExtResource("4")
[node name="SubViewport" type="SubViewport" parent="Healthbar"] [node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718]
transparent_bg = true transparent_bg = true
size = Vector2i(104, 29) size = Vector2i(104, 29)
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"] [node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848]
offset_right = 104.0 offset_right = 104.0
offset_bottom = 29.0 offset_bottom = 29.0
color = Color(1, 0.9, 0.2, 1) color = Color(1, 0.9, 0.2, 1)
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"] [node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403]
offset_left = 2.0 offset_left = 2.0
offset_top = 2.0 offset_top = 2.0
offset_right = 102.0 offset_right = 102.0
offset_bottom = 12.0 offset_bottom = 12.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
max_value = 100.0
value = 100.0 value = 100.0
show_percentage = false show_percentage = false
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport"] [node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036]
offset_left = 2.0 offset_left = 2.0
offset_top = 15.0 offset_top = 15.0
offset_right = 102.0 offset_right = 102.0
@@ -103,5 +95,5 @@ max_value = 50.0
value = 50.0 value = 50.0
show_percentage = false show_percentage = false
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"] [connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"]
[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"] [connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"]

View File

@@ -1,62 +0,0 @@
extends Node
const SPEED := 3.0
const ATTACK_RANGE := 2.0
const REGEN_FAST := 0.10
const REGEN_SLOW := 0.01
@onready var enemy: CharacterBody3D = get_parent()
@onready var nav_agent: NavigationAgent3D = get_parent().get_node("NavigationAgent3D")
func _physics_process(delta: float) -> void:
match enemy.state:
enemy.State.IDLE:
enemy.velocity.x = 0
enemy.velocity.z = 0
enemy.State.CHASE:
_chase()
enemy.State.RETURN:
_return_to_spawn(delta)
func _chase() -> void:
if not is_instance_valid(enemy.target):
enemy.state = enemy.State.RETURN
return
var dist_to_target := enemy.global_position.distance_to(enemy.target.global_position)
if dist_to_target <= ATTACK_RANGE:
enemy.state = enemy.State.ATTACK
return
nav_agent.target_position = enemy.target.global_position
var next_pos := nav_agent.get_next_path_position()
var direction := (next_pos - enemy.global_position).normalized()
direction.y = 0
enemy.velocity.x = direction.x * SPEED
enemy.velocity.z = direction.z * SPEED
func _return_to_spawn(delta: float) -> void:
var dist := enemy.global_position.distance_to(enemy.spawn_position)
if dist < 1.0:
enemy.state = enemy.State.IDLE
enemy.velocity.x = 0
enemy.velocity.z = 0
return
nav_agent.target_position = enemy.spawn_position
var next_pos := nav_agent.get_next_path_position()
var direction := (next_pos - enemy.global_position).normalized()
direction.y = 0
enemy.velocity.x = direction.x * SPEED
enemy.velocity.z = direction.z * SPEED
_regenerate(delta)
func _regenerate(delta: float) -> void:
var health: float = Stats.get_stat(enemy, "health")
var max_health: float = Stats.get_stat(enemy, "max_health")
if health == null or max_health == null:
return
if health < max_health:
var rate: float = REGEN_FAST
if health >= max_health * 0.99:
rate = REGEN_SLOW
health = min(health + max_health * rate * delta, max_health)
Stats.set_stat(enemy, "health", health)
EventBus.health_changed.emit(enemy, health, max_health)

View File

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

30
scenes/enemy/init.gd Normal file
View File

@@ -0,0 +1,30 @@
extends CharacterBody3D
@export var stats: EnemyStats
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
func _ready() -> void:
add_to_group("enemies")
if is_in_group("boss"):
BossData.register(self, stats)
BossData.set_stat(self, "spawn_position", global_position)
else:
EnemyData.register(self, stats)
EnemyData.set_stat(self, "spawn_position", global_position)
EventBus.entity_died.connect(_on_entity_died)
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:
queue_free()
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()

1
scenes/enemy/init.gd.uid Normal file
View File

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

View File

@@ -1,159 +0,0 @@
extends Sprite3D
const ICON_SIZE := 10
const ICON_MARGIN := 1
const BORDER_W := 1
@onready var viewport: SubViewport = $SubViewport
@onready var health_bar: ProgressBar = $SubViewport/HealthBar
@onready var border: ColorRect = $SubViewport/Border
@onready var parent_node: Node = get_parent()
var shield_bar: ProgressBar = null
var style_normal: StyleBoxFlat
var style_aggro: StyleBoxFlat
var effect_container: HBoxContainer = null
var base_viewport_height: int = 0
func _ready() -> void:
shield_bar = $SubViewport.get_node_or_null("ShieldBar")
border.visible = false
style_normal = health_bar.get_theme_stylebox("fill").duplicate()
style_aggro = style_normal.duplicate()
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
base_viewport_height = viewport.size.y
_create_effect_container()
texture = viewport.get_texture()
EventBus.target_changed.connect(_on_target_changed)
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
EventBus.effect_applied.connect(_on_effect_applied)
EventBus.effect_expired.connect(_on_effect_expired)
_init_bars()
func _create_effect_container() -> void:
effect_container = HBoxContainer.new()
effect_container.name = "EffectContainer"
var y_pos: float = 0.0
if shield_bar and shield_bar.visible:
y_pos = shield_bar.offset_bottom + 2
else:
y_pos = health_bar.offset_bottom + 2
effect_container.position = Vector2(2, y_pos)
effect_container.add_theme_constant_override("separation", ICON_MARGIN)
viewport.add_child(effect_container)
func _init_bars() -> void:
var max_health: Variant = Stats.get_stat(parent_node, "max_health")
if max_health != null:
health_bar.max_value = max_health
health_bar.value = Stats.get_stat(parent_node, "health")
var max_shield: Variant = Stats.get_stat(parent_node, "max_shield")
if shield_bar:
if max_shield != null and max_shield > 0:
shield_bar.max_value = max_shield
shield_bar.value = Stats.get_stat(parent_node, "shield")
else:
shield_bar.visible = false
effect_container.position.y = health_bar.offset_bottom + 2
func _process(_delta: float) -> void:
var player: Node = get_tree().get_first_node_in_group("player")
if player and "target" in parent_node and parent_node.target == player:
health_bar.add_theme_stylebox_override("fill", style_aggro)
else:
health_bar.add_theme_stylebox_override("fill", style_normal)
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity != parent_node:
return
health_bar.max_value = max_val
health_bar.value = current
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity != parent_node or shield_bar == null:
return
shield_bar.max_value = max_val
shield_bar.value = current
func _on_target_changed(_player: Node, target: Node) -> void:
border.visible = (target == get_parent())
func _on_effect_applied(target: Node, effect: Effect) -> void:
if target != parent_node:
return
_add_effect_icon(effect)
func _on_effect_expired(target: Node, effect: Effect) -> void:
if target != parent_node:
return
_remove_effect_icon(effect)
func _add_effect_icon(effect: Effect) -> void:
var panel := _create_icon_panel(effect)
var insert_idx: int = _get_sorted_index(effect.type)
effect_container.add_child(panel)
effect_container.move_child(panel, insert_idx)
_resize_viewport()
func _remove_effect_icon(effect: Effect) -> void:
for child in effect_container.get_children():
if child.has_meta("effect_type") and child.has_meta("effect_name"):
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
child.queue_free()
_resize_viewport.call_deferred()
return
func _get_sorted_index(type: int) -> int:
var idx := 0
for child in effect_container.get_children():
if not child.has_meta("effect_type"):
continue
var child_type: int = child.get_meta("effect_type")
if child_type <= type:
idx += 1
else:
break
return idx
func _create_icon_panel(effect: Effect) -> PanelContainer:
var panel := PanelContainer.new()
var style := StyleBoxFlat.new()
match effect.type:
Effect.Type.AURA:
style.bg_color = Color(0.15, 0.15, 0.3, 1)
style.border_color = Color(0.3, 0.5, 1.0, 1)
Effect.Type.BUFF:
style.bg_color = Color(0.15, 0.3, 0.15, 1)
style.border_color = Color(0.3, 1.0, 0.3, 1)
Effect.Type.DEBUFF:
style.bg_color = Color(0.3, 0.15, 0.15, 1)
style.border_color = Color(1.0, 0.3, 0.3, 1)
style.set_border_width_all(BORDER_W)
style.set_content_margin_all(0)
panel.add_theme_stylebox_override("panel", style)
var label := Label.new()
label.text = effect.effect_name.left(1)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 7)
label.add_theme_color_override("font_color", Color.WHITE)
label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
panel.add_child(label)
panel.custom_minimum_size = Vector2(ICON_SIZE + 2, ICON_SIZE + 2)
panel.set_meta("effect_type", effect.type)
panel.set_meta("effect_name", effect.effect_name)
return panel
func _resize_viewport() -> void:
var icon_count := 0
for child in effect_container.get_children():
if not child.is_queued_for_deletion():
icon_count += 1
if icon_count > 0:
var needed: int = int(effect_container.position.y) + ICON_SIZE + 4
viewport.size.y = max(base_viewport_height, needed)
border.offset_bottom = viewport.size.y
else:
viewport.size.y = base_viewport_height
border.offset_bottom = base_viewport_height

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
[gd_scene format=3] [gd_scene format=3]
[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"]
bg_color = Color(0.2, 0.2, 0.2, 0.8) bg_color = Color(0.2, 0.2, 0.2, 0.8)
border_width_bottom = 2 border_width_bottom = 2
@@ -34,8 +32,7 @@ bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1) bg_color = Color(0.2, 0.5, 0.9, 1)
[node name="HUD" type="CanvasLayer"] [node name="HUD" type="CanvasLayer" groups=["hud"]]
script = ExtResource("1")
[node name="HealthBar" type="ProgressBar" parent="."] [node name="HealthBar" type="ProgressBar" parent="."]
offset_left = 10.0 offset_left = 10.0

View File

@@ -5,5 +5,5 @@ extends Node
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
for i in range(5): for i in range(5):
if event.is_action_pressed("ability_%s" % (i + 1)): if event.is_action_pressed("ability_%s" % (i + 1)):
EventBus.ability_use_requested.emit(player, i) EventBus.ability_use.emit(player, i)
return return

View File

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

View File

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

12
scenes/player/init.gd Normal file
View File

@@ -0,0 +1,12 @@
extends CharacterBody3D
@export var stats: PlayerStats
func _ready() -> void:
add_to_group("player")
PlayerData.init_from_resource(stats)
if PlayerData.returning_from_dungeon:
global_position = PlayerData.portal_position + Vector3(0, 1, -5)
PlayerData.returning_from_dungeon = false
elif PlayerData.dungeon_cleared:
PlayerData.clear_cache()

View File

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

View File

@@ -8,12 +8,8 @@ func _physics_process(delta: float) -> void:
if not player.is_on_floor(): if not player.is_on_floor():
player.velocity.y -= gravity * delta player.velocity.y -= gravity * delta
var base: BaseStats = Stats.get_base(player)
var speed: float = base.speed if base is PlayerStats else 5.0
var jump_velocity: float = base.jump_velocity if base is PlayerStats else 4.5
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor(): if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
player.velocity.y = jump_velocity player.velocity.y = PlayerData.jump_velocity
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back") var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var camera_pivot := player.get_node("CameraPivot") as Node3D var camera_pivot := player.get_node("CameraPivot") as Node3D
@@ -27,10 +23,10 @@ func _physics_process(delta: float) -> void:
var direction := (forward * -input_dir.y + right * input_dir.x).normalized() var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
if direction: if direction:
player.velocity.x = direction.x * speed player.velocity.x = direction.x * PlayerData.speed
player.velocity.z = direction.z * speed player.velocity.z = direction.z * PlayerData.speed
else: else:
player.velocity.x = move_toward(player.velocity.x, 0, speed) player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed)
player.velocity.z = move_toward(player.velocity.z, 0, speed) player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed)
player.move_and_slide() player.move_and_slide()

View File

@@ -1,28 +0,0 @@
extends CharacterBody3D
@export var stats: BaseStats
func _ready() -> void:
add_to_group("player")
Stats.register(self, stats)
var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system")
if cooldown_system:
cooldown_system.register(self, 5)
if GameState.returning_from_dungeon:
GameState.restore_player(self)
global_position = GameState.portal_position + Vector3(0, 1, -5)
GameState.returning_from_dungeon = false
elif GameState.dungeon_cleared:
GameState.clear()
var health: float = Stats.get_stat(self, "health")
var max_health: float = Stats.get_stat(self, "max_health")
var shield: float = Stats.get_stat(self, "shield")
var max_shield: float = Stats.get_stat(self, "max_shield")
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
func _exit_tree() -> void:
Stats.deregister(self)
var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system")
if cooldown_system:
cooldown_system.deregister(self)

View File

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

View File

@@ -1,15 +1,12 @@
[gd_scene format=3 uid="uid://cdnkbt1f0db7e"] [gd_scene format=3 uid="uid://cdnkbt1f0db7e"]
[ext_resource type="Script" uid="uid://bfpt2p7uucfyb" path="res://scenes/player/player.gd" id="1"] [ext_resource type="Script" uid="uid://cx6k5473yxno" path="res://scenes/player/init.gd" id="1"]
[ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"] [ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"]
[ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"] [ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"]
[ext_resource type="Script" uid="uid://d15til6fsxw5b" path="res://scenes/player/combat.gd" id="4"] [ext_resource type="Script" uid="uid://hh5yw7vcjdqr" path="res://scenes/player/ability.gd" id="4"]
[ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"] [ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"]
[ext_resource type="Script" path="res://scenes/player/role/role.gd" id="10"] [ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"]
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="11"] [ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"]
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="12"]
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="13"]
[ext_resource type="Resource" path="res://scenes/player/player_stats.tres" id="14"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.3 radius = 0.3
@@ -19,34 +16,31 @@ height = 1.8
radius = 0.3 radius = 0.3
height = 1.8 height = 1.8
[node name="Player" type="CharacterBody3D" unique_id=1350215040] [node name="Player" type="CharacterBody3D" unique_id=197716516]
script = ExtResource("1") script = ExtResource("1")
stats = ExtResource("14") stats = ExtResource("14")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=311205642]
shape = SubResource("CapsuleShape3D_1") shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1346931672] [node name="Mesh" type="MeshInstance3D" parent="." unique_id=1514179122]
mesh = SubResource("CapsuleMesh_1") mesh = SubResource("CapsuleMesh_1")
[node name="CameraPivot" type="Node3D" parent="." unique_id=1292689540] [node name="CameraPivot" type="Node3D" parent="." unique_id=1881685457]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
script = ExtResource("2") script = ExtResource("2")
[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=1225820651] [node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=2062990383]
transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5) transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5)
[node name="Movement" type="Node" parent="." unique_id=654979387] [node name="Movement" type="Node" parent="." unique_id=811179177]
script = ExtResource("3") script = ExtResource("3")
[node name="Combat" type="Node" parent="." unique_id=1754235583] [node name="Ability" type="Node" parent="." unique_id=1184596245]
script = ExtResource("4") script = ExtResource("4")
[node name="Targeting" type="Node" parent="." unique_id=592540710] [node name="Targeting" type="Node" parent="." unique_id=1974574662]
script = ExtResource("8") script = ExtResource("8")
[node name="Role" type="Node" parent="." unique_id=134158295] [node name="Role" type="Node" parent="." unique_id=1637643687]
script = ExtResource("10") script = ExtResource("10")
tank_set = ExtResource("11")
damage_set = ExtResource("12")
healer_set = ExtResource("13")

View File

@@ -1,40 +1,14 @@
extends Node extends Node
enum Role { TANK, DAMAGE, HEALER }
var current_role: int = Role.DAMAGE
@export var tank_set: AbilitySet
@export var damage_set: AbilitySet
@export var healer_set: AbilitySet
@onready var player: CharacterBody3D = get_parent() @onready var player: CharacterBody3D = get_parent()
func _ready() -> void: func _ready() -> void:
set_role.call_deferred(current_role) EventBus.role_change_requested.emit(player, PlayerData.current_role)
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("class_tank"): if event.is_action_pressed("class_tank"):
set_role(Role.TANK) EventBus.role_change_requested.emit(player, PlayerData.Role.TANK)
elif event.is_action_pressed("class_damage"): elif event.is_action_pressed("class_damage"):
set_role(Role.DAMAGE) EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE)
elif event.is_action_pressed("class_healer"): elif event.is_action_pressed("class_healer"):
set_role(Role.HEALER) EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER)
func set_role(new_role: int) -> void:
current_role = new_role
EventBus.role_changed.emit(player, current_role)
func get_role_icon() -> String:
match current_role:
Role.TANK: return "T"
Role.DAMAGE: return "D"
Role.HEALER: return "H"
return ""
func get_ability_set() -> AbilitySet:
match current_role:
Role.TANK: return tank_set
Role.DAMAGE: return damage_set
Role.HEALER: return healer_set
return damage_set

View File

@@ -1,27 +1,12 @@
extends Node extends Node
const TARGET_RANGE := 20.0 const TARGET_RANGE := 20.0
const COMBAT_TIMEOUT := 3.0
var current_target: Node3D = null
var mouse_press_pos: Vector2 = Vector2.ZERO var mouse_press_pos: Vector2 = Vector2.ZERO
var in_combat := false
var combat_timer := 0.0
@onready var player: CharacterBody3D = get_parent() @onready var player: CharacterBody3D = get_parent()
@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D") @onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D")
func _ready() -> void:
EventBus.damage_dealt.connect(_on_damage_dealt)
EventBus.entity_died.connect(_on_entity_died)
EventBus.enemy_engaged.connect(_on_enemy_engaged)
func _process(delta: float) -> void:
if in_combat:
combat_timer -= delta
if combat_timer <= 0:
in_combat = false
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed: if event.pressed:
@@ -44,71 +29,19 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void:
var result := space.intersect_ray(query) var result := space.intersect_ray(query)
if result: if result:
var hit_target := result.collider.get_parent() as Node3D var hit_target := result.collider.get_parent() as Node3D
set_target(hit_target) EventBus.target_requested.emit(player, hit_target)
else: else:
set_target(null) EventBus.target_requested.emit(player, null)
func _cycle_target() -> void: func _cycle_target() -> void:
var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
if targets.is_empty(): if targets.is_empty():
set_target(null) EventBus.target_requested.emit(player, null)
return return
if current_target == null or current_target not in targets: var current: Node3D = PlayerData.target
set_target(targets[0]) if current == null or current not in targets:
EventBus.target_requested.emit(player, targets[0])
return return
var idx := targets.find(current_target) var idx := targets.find(current)
var next_idx := (idx + 1) % targets.size() var next_idx := (idx + 1) % targets.size()
set_target(targets[next_idx]) EventBus.target_requested.emit(player, targets[next_idx])
func set_target(target: Node3D) -> void:
current_target = target
EventBus.target_changed.emit(player, target)
func _on_enemy_engaged(_enemy: Node, target: Node) -> void:
if target == player:
combat_timer = COMBAT_TIMEOUT
if not in_combat:
in_combat = true
if current_target == null:
_auto_target()
func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void:
if target == player:
combat_timer = COMBAT_TIMEOUT
if not in_combat:
in_combat = true
if current_target == null:
_auto_target()
elif attacker == player and in_combat:
combat_timer = COMBAT_TIMEOUT
func _on_entity_died(entity: Node) -> void:
if entity == current_target:
set_target(null)
if in_combat:
_auto_target(entity)
func _auto_target(exclude: Node = null) -> void:
# Priorität 1: Nächster Gegner
var enemies := get_tree().get_nodes_in_group("enemies")
var nearest: Node3D = null
var nearest_dist: float = INF
for enemy in enemies:
if is_instance_valid(enemy) and enemy != exclude:
var dist: float = player.global_position.distance_to(enemy.global_position)
if dist < nearest_dist:
nearest_dist = dist
nearest = enemy
if nearest:
set_target(nearest)
return
# Priorität 2: Nächstes Portal
var portals := get_tree().get_nodes_in_group("portals")
for p in portals:
if is_instance_valid(p) and p != exclude:
var dist: float = player.global_position.distance_to(p.global_position)
if dist < nearest_dist:
nearest_dist = dist
nearest = p
if nearest:
set_target(nearest)

View File

@@ -7,7 +7,7 @@ var active := false
func _ready() -> void: func _ready() -> void:
if not is_exit: if not is_exit:
if GameState.dungeon_cleared: if PlayerData.dungeon_cleared:
queue_free() queue_free()
return return
get_tree().create_timer(0.5).timeout.connect(_check_overlapping) get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
@@ -23,11 +23,11 @@ func _on_gate_area_body_entered(body: Node3D) -> void:
if not active: if not active:
return return
if body is CharacterBody3D and body.name == "Player": if body is CharacterBody3D and body.name == "Player":
GameState.save_player(body) PlayerData.save_cache()
if is_exit: if is_exit:
GameState.returning_from_dungeon = true PlayerData.returning_from_dungeon = true
else: else:
GameState.portal_position = global_position PlayerData.portal_position = global_position
call_deferred("_change_scene") call_deferred("_change_scene")
func _change_scene() -> void: func _change_scene() -> void:

14
scenes/portal/init.gd Normal file
View File

@@ -0,0 +1,14 @@
extends StaticBody3D
@export var stats: PortalStats
func _ready() -> void:
add_to_group("portals")
PortalData.register(self, stats)
func _exit_tree() -> void:
PortalData.deregister(self)
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.portal_entered.emit(self, body)

View File

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

View File

@@ -1,33 +0,0 @@
extends StaticBody3D
@export var stats: BaseStats
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
func _ready() -> void:
add_to_group("portals")
Stats.register(self, stats)
EventBus.entity_died.connect(_on_entity_died)
func _exit_tree() -> void:
Stats.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return
if not is_inside_tree():
return
var pos: Vector3 = global_position
var gate: Node3D = GATE_SCENE.instantiate()
get_parent().add_child(gate)
gate.global_position = pos
var enemies := get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if is_instance_valid(enemy):
enemy.queue_free()
EventBus.portal_defeated.emit(self)
queue_free()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_engaged.emit(self, body)

View File

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

View File

@@ -1,7 +1,6 @@
[gd_scene format=3] [gd_scene format=3]
[ext_resource type="Script" path="res://scenes/portal/portal.gd" id="1"] [ext_resource type="Script" path="res://scenes/portal/init.gd" id="1"]
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="3"]
[ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"] [ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"]
[sub_resource type="SphereShape3D" id="SphereShape3D_1"] [sub_resource type="SphereShape3D" id="SphereShape3D_1"]
@@ -59,7 +58,6 @@ shape = SubResource("SphereShape3D_1")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
billboard = 1 billboard = 1
pixel_size = 0.01 pixel_size = 0.01
script = ExtResource("3")
[node name="SubViewport" type="SubViewport" parent="Healthbar"] [node name="SubViewport" type="SubViewport" parent="Healthbar"]
transparent_bg = true transparent_bg = true

View File

@@ -11,17 +11,17 @@ var portals: Array[Node] = []
var timer := 0.0 var timer := 0.0
func _ready() -> void: func _ready() -> void:
if GameState.portal_position != Vector3.ZERO and not GameState.dungeon_cleared: if PlayerData.portal_position != Vector3.ZERO and not PlayerData.dungeon_cleared:
call_deferred("_restore_gate") call_deferred("_restore_gate")
else: else:
if GameState.dungeon_cleared: if PlayerData.dungeon_cleared:
GameState.clear() PlayerData.clear_cache()
call_deferred("_spawn_portal") call_deferred("_spawn_portal")
func _restore_gate() -> void: func _restore_gate() -> void:
var gate: Node3D = GATE_SCENE.instantiate() var gate: Node3D = GATE_SCENE.instantiate()
get_parent().add_child(gate) get_parent().add_child(gate)
gate.global_position = GameState.portal_position gate.global_position = PlayerData.portal_position
func _process(delta: float) -> void: func _process(delta: float) -> void:
timer += delta timer += delta

View File

@@ -1,22 +1,34 @@
[gd_scene format=3 uid="uid://dy1icabu2ssbw"] [gd_scene format=3 uid="uid://dy1icabu2ssbw"]
[ext_resource type="Script" uid="uid://h0hts425epc6" path="res://systems/ability_system.gd" id="ability_system"] [ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
[ext_resource type="Script" uid="uid://cysg30lud2ta2" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] [ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
[ext_resource type="Script" uid="uid://cyffo1g4uhmwh" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] [ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
[ext_resource type="Script" uid="uid://cm7ehl2pexcst" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] [ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
[ext_resource type="Script" uid="uid://c7gsu2qddsor6" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] [ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
[ext_resource type="Script" uid="uid://ddos7mo8rahou" path="res://systems/cooldown_system.gd" id="cooldown_system"] [ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
[ext_resource type="Script" uid="uid://cbd1bryh0e2dw" path="res://systems/damage_system.gd" id="damage_system"] [ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
[ext_resource type="Script" uid="uid://drdlh6tq0dfwo" path="res://systems/effect_system.gd" id="effect_system"] [ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"]
[ext_resource type="Script" uid="uid://bqebxfvticxto" path="res://systems/element_system.gd" id="element_system"] [ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
[ext_resource type="Script" uid="uid://bwhxu5586lc1l" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"] [ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
[ext_resource type="Script" uid="uid://b3wkn5118dimy" path="res://systems/health_system.gd" id="health_system"] [ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
[ext_resource type="Script" path="res://systems/portal_system.gd" id="portal_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="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/shield_system.gd" id="shield_system"]
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
[ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] [ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"]
[ext_resource type="Script" uid="uid://cskx6o07iukwh" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"] [ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"]
[ext_resource type="Script" uid="uid://b1qkvoqvmd21h" path="res://systems/respawn_system.gd" id="respawn_system"] [ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"]
[ext_resource type="Script" uid="uid://rsnpuf77o0sn" path="res://systems/shield_system.gd" id="shield_system"] [ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"]
[ext_resource type="Script" uid="uid://c84voxmnaifyt" path="res://systems/spawn_system.gd" id="spawn_system"] [ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"]
[sub_resource type="NavigationMesh" id="NavigationMesh_1"] [sub_resource type="NavigationMesh" id="NavigationMesh_1"]
vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5) vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5)
@@ -53,81 +65,111 @@ size = Vector3(5, 3, 5)
[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"] [sub_resource type="BoxShape3D" id="BoxShape3D_tavern"]
size = Vector3(5, 3, 5) size = Vector3(5, 3, 5)
[node name="World" type="Node3D" unique_id=1865233338] [node name="World" type="Node3D"]
[node name="Systems" type="Node" parent="." unique_id=1813416478] [node name="Systems" type="Node" parent="."]
[node name="HealthSystem" type="Node" parent="Systems" unique_id=221270411] [node name="HealthSystem" type="Node" parent="Systems"]
script = ExtResource("health_system") script = ExtResource("health_system")
[node name="ShieldSystem" type="Node" parent="Systems" unique_id=1790230220] [node name="DamageSystem" type="Node" parent="Systems"]
script = ExtResource("shield_system")
[node name="DamageSystem" type="Node" parent="Systems" unique_id=2146323526]
script = ExtResource("damage_system") script = ExtResource("damage_system")
[node name="AbilitySystem" type="Node" parent="Systems" unique_id=391120092] [node name="HealSystem" type="Node" parent="Systems"]
script = ExtResource("heal_system")
[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") script = ExtResource("ability_system")
[node name="CooldownSystem" type="Node" parent="Systems" unique_id=99457358] [node name="AttackSystem" type="Node" parent="Systems"]
script = ExtResource("attack_system")
[node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("cooldown_system") script = ExtResource("cooldown_system")
[node name="AggroSystem" type="Node" parent="Systems" unique_id=1539448343] [node name="TargetingSystem" type="Node" parent="Systems"]
script = ExtResource("targeting_system")
[node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("aggro_system") script = ExtResource("aggro_system")
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem" unique_id=1597893665] [node name="AggroTracker" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_tracker") script = ExtResource("aggro_tracker")
[node name="AggroDecay" type="Node" parent="Systems/AggroSystem" unique_id=1571705506] [node name="AggroDecay" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_decay") script = ExtResource("aggro_decay")
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem" unique_id=1936723580] [node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_events") script = ExtResource("aggro_events")
[node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042] [node name="AISystem" type="Node" parent="Systems"]
script = ExtResource("enemy_ai_system") script = ExtResource("ai_system")
[node name="RespawnSystem" type="Node" parent="Systems" unique_id=1586865573] [node name="RespawnSystem" type="Node" parent="Systems"]
script = ExtResource("respawn_system") script = ExtResource("respawn_system")
[node name="SpawnSystem" type="Node" parent="Systems" unique_id=1099032666] [node name="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("spawn_system") script = ExtResource("spawn_system")
[node name="EffectSystem" type="Node" parent="Systems" unique_id=1219368182] [node name="PortalSystem" type="Node" parent="Systems"]
script = ExtResource("effect_system") script = ExtResource("portal_system")
[node name="ElementSystem" type="Node" parent="Systems" unique_id=1401212832] [node name="AuraSystem" type="Node" parent="Systems"]
script = ExtResource("aura_system")
[node name="BuffSystem" type="Node" parent="Systems"]
script = ExtResource("buff_system")
[node name="DebuffSystem" type="Node" parent="Systems"]
script = ExtResource("debuff_system")
[node name="ElementSystem" type="Node" parent="Systems"]
script = ExtResource("element_system") script = ExtResource("element_system")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679] [node name="HudSystem" type="Node" parent="Systems"]
script = ExtResource("hud_system")
[node name="NameplateSystem" type="Node" parent="Systems"]
script = ExtResource("nameplate_system")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506)
navigation_mesh = SubResource("NavigationMesh_1") navigation_mesh = SubResource("NavigationMesh_1")
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=593226019] [node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"]
mesh = SubResource("PlaneMesh_1") mesh = SubResource("PlaneMesh_1")
[node name="BodenCollision" type="StaticBody3D" parent="." unique_id=1112667638] [node name="BodenCollision" type="StaticBody3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision" unique_id=621554623] [node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
shape = SubResource("WorldBoundaryShape3D_1") shape = SubResource("WorldBoundaryShape3D_1")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1797472817] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0) transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
shadow_enabled = true shadow_enabled = true
[node name="Taverne" type="StaticBody3D" parent="." unique_id=1978646562] [node name="Taverne" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="Mesh" type="MeshInstance3D" parent="Taverne" unique_id=2043279810] [node name="Mesh" type="MeshInstance3D" parent="Taverne"]
mesh = SubResource("BoxMesh_tavern") mesh = SubResource("BoxMesh_tavern")
[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne" unique_id=2108564286] [node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne"]
shape = SubResource("BoxShape3D_tavern") shape = SubResource("BoxShape3D_tavern")
[node name="Player" parent="." unique_id=585018813 instance=ExtResource("player")] [node name="Player" parent="." instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5)
[node name="HUD" parent="." unique_id=763693646 instance=ExtResource("hud")] [node name="HUD" parent="." instance=ExtResource("hud")]
[node name="PortalSpawner" type="Node" parent="." unique_id=2100621428] [node name="PortalSpawner" type="Node" parent="."]
script = ExtResource("portal_spawner") script = ExtResource("portal_spawner")

View File

@@ -1,124 +1,83 @@
extends Node extends Node
func _ready() -> void: func _ready() -> void:
EventBus.ability_use_requested.connect(_on_ability_use_requested) EventBus.ability_use.connect(_on_ability_use)
func _process(_delta: float) -> void: func _on_ability_use(_player: Node, ability_index: int) -> void:
var players := get_tree().get_nodes_in_group("player") if not PlayerData.alive:
for player in players:
if not Stats.is_alive(player):
continue
_try_auto_attack(player)
func _try_auto_attack(player: Node) -> void:
var targeting: Node = player.get_node_or_null("Targeting")
if not targeting or not targeting.in_combat or not targeting.current_target:
return return
if not is_instance_valid(targeting.current_target): var ability_set: AbilitySet = PlayerData.ability_set
return
var cooldown_system: Node = get_node("../CooldownSystem")
if not cooldown_system.is_aa_ready(player):
return
var role: Node = player.get_node("Role")
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
return
var aa_damage: float = ability_set.aa_damage
var aa_range: float = ability_set.aa_range
var aa_is_heal: bool = ability_set.aa_is_heal
var dmg: float = _apply_passive(player, aa_damage, "heal" if aa_is_heal else "damage")
if aa_is_heal:
EventBus.heal_requested.emit(player, player, dmg)
else:
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
if dist > aa_range:
return
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
var player_base: BaseStats = Stats.get_base(player)
var aa_cd: float = player_base.aa_cooldown if player_base is PlayerStats else 0.5
cooldown_system.set_aa_cooldown(player, aa_cd)
func _on_ability_use_requested(player: Node, ability_index: int) -> void:
var role: Node = player.get_node_or_null("Role")
if not role:
return
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set or ability_index >= ability_set.abilities.size(): if not ability_set or ability_index >= ability_set.abilities.size():
return return
var ability: Ability = ability_set.abilities[ability_index] var ability: Ability = ability_set.abilities[ability_index]
if not ability or ability.type == Ability.Type.PASSIVE: if not ability or ability.type == Ability.Type.PASSIVE:
return return
var cooldown_system: Node = get_node("../CooldownSystem") if PlayerData.cooldowns[ability_index] > 0:
if not cooldown_system.is_ready(player, ability_index):
return return
if ability.uses_gcd and not cooldown_system.is_gcd_ready(player): if ability.uses_gcd and PlayerData.gcd > 0:
return return
var success: bool = _execute_ability(player, ability) var success: bool = _execute_ability(ability)
if not success: if not success:
return return
var player_base: BaseStats = Stats.get_base(player) var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0
var gcd_time: float = player_base.gcd_time if player_base is PlayerStats else 0.5 PlayerData.cooldowns[ability_index] = ability.cooldown
var gcd: float = gcd_time if ability.uses_gcd else 0.0 PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd)
cooldown_system.set_cooldown(player, ability_index, ability.cooldown, gcd) if gcd > 0:
PlayerData.gcd = gcd
func _execute_ability(player: Node, ability: Ability) -> bool: func _execute_ability(ability: Ability) -> bool:
var targeting: Node = player.get_node("Targeting")
var stat: String = "heal" if ability.is_heal else "damage" var stat: String = "heal" if ability.is_heal else "damage"
var dmg: float = _apply_passive(player, ability.damage, stat) var dmg: float = _apply_passive(ability.damage, stat)
var player: Node = get_tree().get_first_node_in_group("player")
match ability.type: match ability.type:
Ability.Type.SINGLE: Ability.Type.SINGLE:
return _execute_single(player, targeting, ability, dmg) return _execute_single(player, ability, dmg)
Ability.Type.AOE: Ability.Type.AOE:
return _execute_aoe(player, ability, dmg) return _execute_aoe(player, ability, dmg)
Ability.Type.UTILITY: Ability.Type.UTILITY:
return _execute_utility(player, ability) return _execute_utility(ability)
Ability.Type.ULT: Ability.Type.ULT:
return _execute_ult(player, targeting, ability, dmg) return _execute_ult(player, ability, dmg)
return false return false
func _apply_passive(player: Node, base: float, stat: String) -> float: func _apply_passive(base: float, stat: String) -> float:
var mult: Variant = Stats.get_stat(player, "buff_" + stat) var mult: float = 1.0
if mult != null: match stat:
return base * mult "damage": mult = PlayerData.buff_damage
return base "heal": mult = PlayerData.buff_heal
return base * mult
func _in_range(player: Node, targeting: Node, ability: Ability) -> bool: func _in_range(ability: Ability) -> bool:
if ability.ability_range <= 0 or ability.is_heal: if ability.ability_range <= 0 or ability.is_heal:
return true return true
if not is_instance_valid(targeting.current_target): if not is_instance_valid(PlayerData.target):
return false return false
var dist: float = player.global_position.distance_to(targeting.current_target.global_position) var player: Node = get_tree().get_first_node_in_group("player")
var dist: float = player.global_position.distance_to(PlayerData.target.global_position)
return dist <= ability.ability_range return dist <= ability.ability_range
func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: func _execute_single(player: Node, ability: Ability, dmg: float) -> bool:
if ability.is_heal: if ability.is_heal:
EventBus.heal_requested.emit(player, player, dmg) EventBus.heal_requested.emit(player, player, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true return true
if not _in_range(player, targeting, ability): if not _in_range(ability):
return false return false
if not is_instance_valid(targeting.current_target): if not is_instance_valid(PlayerData.target):
return false return false
EventBus.damage_requested.emit(player, targeting.current_target, dmg) EventBus.damage_requested.emit(player, PlayerData.target, dmg)
if ability.element != 0: if ability.element != 0:
EventBus.element_damage_dealt.emit(player, targeting.current_target, dmg, ability.element) EventBus.element_damage_dealt.emit(player, PlayerData.target, dmg, ability.element)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true return true
func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
if ability.is_heal: if ability.is_heal:
EventBus.heal_requested.emit(player, player, dmg) EventBus.heal_requested.emit(player, player, dmg)
var players := get_tree().get_nodes_in_group("player")
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= ability.ability_range:
EventBus.heal_requested.emit(player, p, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true return true
var hit := false var hit := false
var enemies := get_tree().get_nodes_in_group("enemies") for enemy in get_tree().get_nodes_in_group("enemies"):
for enemy in enemies:
var dist: float = player.global_position.distance_to(enemy.global_position) var dist: float = player.global_position.distance_to(enemy.global_position)
if dist <= ability.ability_range: if dist <= ability.ability_range:
EventBus.damage_requested.emit(player, enemy, dmg) EventBus.damage_requested.emit(player, enemy, dmg)
@@ -129,44 +88,35 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return hit return hit
func _execute_utility(player: Node, ability: Ability) -> bool: func _execute_utility(ability: Ability) -> bool:
var max_shield: float = Stats.get_stat(player, "max_shield") if PlayerData.max_shield <= 0:
if max_shield <= 0:
return false return false
var shield: float = Stats.get_stat(player, "shield") var shield: float = PlayerData.shield
if ability.damage > 0: if ability.damage > 0:
shield = max_shield * (ability.damage / 100.0) shield = PlayerData.max_shield * (ability.damage / 100.0)
else: else:
if shield >= max_shield: if shield >= PlayerData.max_shield:
return false return false
shield = max_shield shield = PlayerData.max_shield
Stats.set_stat(player, "shield", shield) PlayerData.set_shield(shield)
EventBus.shield_changed.emit(player, shield, max_shield)
return true return true
func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool:
if ability.is_heal: if ability.is_heal:
EventBus.heal_requested.emit(player, player, dmg) EventBus.heal_requested.emit(player, player, dmg)
var players := get_tree().get_nodes_in_group("player")
var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= aoe_range:
EventBus.heal_requested.emit(player, p, dmg * 0.4)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true return true
if not _in_range(player, targeting, ability): if not _in_range(ability):
return false return false
if not is_instance_valid(targeting.current_target): if not is_instance_valid(PlayerData.target):
return false return false
var target: Node3D = targeting.current_target var target: Node3D = PlayerData.target
EventBus.damage_requested.emit(player, target, dmg * 5.0) EventBus.damage_requested.emit(player, target, dmg * 5.0)
if ability.element != 0: if ability.element != 0:
EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element) EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element)
var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
var enemies := get_tree().get_nodes_in_group("enemies") for enemy in get_tree().get_nodes_in_group("enemies"):
for enemy in enemies:
if enemy != target and is_instance_valid(enemy): if enemy != target and is_instance_valid(enemy):
var enemy_dist: float = target.global_position.distance_to(enemy.global_position) var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
if enemy_dist <= splash_range: if enemy_dist <= splash_range:

View File

@@ -23,8 +23,12 @@ func _update_combat_timers(delta: float) -> void:
func _decay_aggro(enemy: Node, delta: float) -> void: func _decay_aggro(enemy: Node, delta: float) -> void:
var table: Dictionary = tracker.aggro_tables[enemy] var table: Dictionary = tracker.aggro_tables[enemy]
var base: BaseStats = Stats.get_base(enemy) var data_source: Node = tracker._get_data_source(enemy)
var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0 var aggro_decay: float = 1.0
if data_source:
var base: EnemyStats = data_source.get_base(enemy)
if base:
aggro_decay = base.aggro_decay
for player in table.keys(): for player in table.keys():
if is_in_combat(player): if is_in_combat(player):
continue continue
@@ -58,10 +62,11 @@ func spread_aggro(source: Node, attacker: Node, amount: float) -> void:
func alert_nearby(enemy: Node, target: Node) -> void: func alert_nearby(enemy: Node, target: Node) -> void:
var radius: float = tracker.get_alert_radius(enemy) var radius: float = tracker.get_alert_radius(enemy)
for other in tracker.get_enemies_in_radius(enemy, radius): for other in tracker.get_enemies_in_radius(enemy, radius):
if "state" in other and other.state == other.State.IDLE: var data_source: Node = tracker._get_data_source(other)
if data_source and data_source.get_stat(other, "state") == 0:
tracker.add_aggro(other, target, 1.0) tracker.add_aggro(other, target, 1.0)
other.target = target data_source.set_stat(other, "target", target)
other.state = other.State.CHASE data_source.set_stat(other, "state", 1)
EventBus.enemy_engaged.emit(other, target) EventBus.enemy_engaged.emit(other, target)
func erase_entity(entity: Node) -> void: func erase_entity(entity: Node) -> void:

View File

@@ -14,14 +14,16 @@ func _ready() -> void:
func _on_enemy_detected(enemy: Node, player: Node) -> void: func _on_enemy_detected(enemy: Node, player: Node) -> void:
if not enemy.is_in_group("enemies"): if not enemy.is_in_group("enemies"):
return return
if "state" in enemy: var data_source: Node = tracker._get_data_source(enemy)
if enemy.state == enemy.State.CHASE or enemy.state == enemy.State.ATTACK: if data_source:
var state: int = data_source.get_stat(enemy, "state")
if state == 1 or state == 2:
return return
tracker.add_player_in_range(enemy, player) tracker.add_player_in_range(enemy, player)
tracker.add_aggro(enemy, player, 1.0) tracker.add_aggro(enemy, player, 1.0)
if "state" in enemy: if data_source:
enemy.target = player data_source.set_stat(enemy, "target", player)
enemy.state = enemy.State.CHASE data_source.set_stat(enemy, "state", 1)
EventBus.enemy_engaged.emit(enemy, player) EventBus.enemy_engaged.emit(enemy, player)
decay.alert_nearby(enemy, player) decay.alert_nearby(enemy, player)
@@ -33,8 +35,7 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
return return
decay.reset_combat_timer(attacker) decay.reset_combat_timer(attacker)
var multiplier := 1.0 var multiplier := 1.0
var role: Node = attacker.get_node_or_null("Role") if PlayerData.current_role == PlayerData.Role.TANK:
if role and role.current_role == 0:
multiplier = config.tank_multiplier multiplier = config.tank_multiplier
var aggro: float = amount * multiplier var aggro: float = amount * multiplier
tracker.add_aggro(target, attacker, aggro) tracker.add_aggro(target, attacker, aggro)

View File

@@ -43,17 +43,19 @@ func get_top_target(table: Dictionary) -> Node:
return top return top
func update_target(enemy: Node) -> void: func update_target(enemy: Node) -> void:
if not "state" in enemy:
return
var table: Dictionary = aggro_tables[enemy] var table: Dictionary = aggro_tables[enemy]
var top: Node = get_top_target(table) var top: Node = get_top_target(table)
if top and top != enemy.target: var data_source: Node = _get_data_source(enemy)
enemy.target = top if not data_source:
if enemy.state == enemy.State.IDLE or enemy.state == enemy.State.RETURN: return
enemy.state = enemy.State.CHASE var state: int = data_source.get_stat(enemy, "state")
elif not top and enemy.state != enemy.State.IDLE and enemy.state != enemy.State.RETURN: if top and top != data_source.get_stat(enemy, "target"):
enemy.target = null data_source.set_stat(enemy, "target", top)
enemy.state = enemy.State.RETURN if state == 0 or state == 3:
data_source.set_stat(enemy, "state", 1)
elif not top and state != 0 and state != 3:
data_source.set_stat(enemy, "target", null)
data_source.set_stat(enemy, "state", 3)
func get_enemies_in_radius(source: Node, radius: float) -> Array: func get_enemies_in_radius(source: Node, radius: float) -> Array:
var result: Array = [] var result: Array = []
@@ -65,8 +67,12 @@ func get_enemies_in_radius(source: Node, radius: float) -> Array:
return result return result
func get_alert_radius(entity: Node) -> float: func get_alert_radius(entity: Node) -> float:
var base: BaseStats = Stats.get_base(entity) var data_source: Node = _get_data_source(entity)
return base.alert_radius if base is EnemyStats else 10.0 if data_source:
var base: EnemyStats = data_source.get_base(entity)
if base:
return base.alert_radius
return 10.0
func erase_entity(entity: Node) -> void: func erase_entity(entity: Node) -> void:
aggro_tables.erase(entity) aggro_tables.erase(entity)
@@ -74,9 +80,17 @@ func erase_entity(entity: Node) -> void:
for enemy in aggro_tables: for enemy in aggro_tables:
if is_instance_valid(enemy): if is_instance_valid(enemy):
aggro_tables[enemy].erase(entity) aggro_tables[enemy].erase(entity)
if "target" in enemy and entity == enemy.target: var data_source: Node = _get_data_source(enemy)
enemy.target = null if data_source and data_source.get_stat(enemy, "target") == entity:
enemy.state = enemy.State.RETURN data_source.set_stat(enemy, "target", null)
data_source.set_stat(enemy, "state", 3)
for enemy in players_in_range: for enemy in players_in_range:
if is_instance_valid(enemy): if is_instance_valid(enemy):
players_in_range[enemy].erase(entity) players_in_range[enemy].erase(entity)
func _get_data_source(entity: Node) -> Node:
if entity.is_in_group("boss"):
return BossData
elif entity.is_in_group("enemies"):
return EnemyData
return null

91
systems/ai_system.gd Normal file
View File

@@ -0,0 +1,91 @@
extends Node
enum State { IDLE, CHASE, ATTACK, RETURN }
func _physics_process(delta: float) -> void:
_process_group(delta, EnemyData)
_process_group(delta, BossData)
func _process_group(delta: float, data_source: Node) -> void:
for entity in data_source.entities:
if not is_instance_valid(entity) or not data_source.is_alive(entity):
continue
var data: Dictionary = data_source.entities[entity]
var state: int = data["state"]
match state:
State.IDLE:
entity.velocity.x = 0
entity.velocity.z = 0
State.CHASE:
_chase(entity, data, data_source)
State.ATTACK:
_attack(entity, data, data_source, delta)
State.RETURN:
_return_to_spawn(entity, data, data_source, delta)
func _chase(entity: Node, data: Dictionary, data_source: Node) -> void:
if not is_instance_valid(data["target"]):
data["state"] = State.RETURN
return
var base: EnemyStats = data_source.get_base(entity)
var attack_range: float = base.attack_range
var dist: float = entity.global_position.distance_to(data["target"].global_position)
if dist <= attack_range:
data["state"] = State.ATTACK
return
var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D")
if not nav_agent:
return
nav_agent.target_position = data["target"].global_position
var next_pos := nav_agent.get_next_path_position()
var direction: Vector3 = (next_pos - entity.global_position).normalized()
direction.y = 0
entity.velocity.x = direction.x * base.speed
entity.velocity.z = direction.z * base.speed
func _attack(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
data["attack_timer"] -= delta
if not is_instance_valid(data["target"]):
data["state"] = State.RETURN
return
var base: EnemyStats = data_source.get_base(entity)
var dist: float = entity.global_position.distance_to(data["target"].global_position)
if dist > base.attack_range:
data["state"] = State.CHASE
return
if data["attack_timer"] <= 0:
data["attack_timer"] = base.attack_cooldown
EventBus.damage_requested.emit(entity, data["target"], base.attack_damage)
entity.velocity.x = 0
entity.velocity.z = 0
func _return_to_spawn(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
var spawn_pos: Vector3 = data["spawn_position"]
var dist: float = entity.global_position.distance_to(spawn_pos)
if dist < 1.0:
data["state"] = State.IDLE
entity.velocity.x = 0
entity.velocity.z = 0
return
var base: EnemyStats = data_source.get_base(entity)
var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D")
if not nav_agent:
return
nav_agent.target_position = spawn_pos
var next_pos := nav_agent.get_next_path_position()
var direction: Vector3 = (next_pos - entity.global_position).normalized()
direction.y = 0
entity.velocity.x = direction.x * base.speed
entity.velocity.z = direction.z * base.speed
_regenerate(entity, data, data_source, delta)
func _regenerate(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
var health: float = data["health"]
var max_health: float = data["max_health"]
if health < max_health:
var base: EnemyStats = data_source.get_base(entity)
var rate: float = base.regen_fast
if health >= max_health * 0.99:
rate = base.regen_slow
health = min(health + max_health * rate * delta, max_health)
data_source.set_health(entity, health)

1
systems/ai_system.gd.uid Normal file
View File

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

27
systems/attack_system.gd Normal file
View File

@@ -0,0 +1,27 @@
extends Node
func _process(_delta: float) -> void:
if not PlayerData.alive or not PlayerData.in_combat:
return
if not is_instance_valid(PlayerData.target):
return
if PlayerData.aa_timer > 0:
return
var ability_set: AbilitySet = PlayerData.ability_set
if not ability_set:
return
var player: Node = get_tree().get_first_node_in_group("player")
if not player:
return
var aa_damage: float = ability_set.aa_damage
var aa_range: float = ability_set.aa_range
var aa_is_heal: bool = ability_set.aa_is_heal
var dmg: float = aa_damage * (PlayerData.buff_heal if aa_is_heal else PlayerData.buff_damage)
if aa_is_heal:
EventBus.heal_requested.emit(player, player, dmg)
else:
var dist: float = player.global_position.distance_to(PlayerData.target.global_position)
if dist > aa_range:
return
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
PlayerData.aa_timer = PlayerData.aa_cooldown

View File

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

62
systems/aura_system.gd Normal file
View File

@@ -0,0 +1,62 @@
extends Node
const AURA_REFRESH := 0.5
var active_auras: Dictionary = {}
func _ready() -> void:
EventBus.role_changed.connect(_on_role_changed)
EventBus.entity_died.connect(_on_entity_died)
func _process(_delta: float) -> void:
for entity in active_auras.keys():
if not is_instance_valid(entity):
active_auras.erase(entity)
continue
for aura in active_auras[entity]:
_propagate(entity, aura)
func _propagate(source_entity: Node, aura: Effect) -> void:
if not source_entity is Node3D:
return
var buff_system: Node = get_node("../BuffSystem")
var players := get_tree().get_nodes_in_group("player")
for player in players:
if not is_instance_valid(player) or not PlayerData.alive:
continue
var dist: float = source_entity.global_position.distance_to(player.global_position)
if dist > aura.aura_radius:
continue
if buff_system.has_aura_buff(player, aura.effect_name, source_entity):
buff_system.refresh_aura_buff(player, aura.effect_name, source_entity, AURA_REFRESH)
else:
var buff := Effect.new()
buff.effect_name = aura.effect_name
buff.type = Effect.Type.BUFF
buff.stat = aura.stat
buff.value = aura.value
buff.duration = AURA_REFRESH
buff.is_multiplier = aura.is_multiplier
buff_system.apply_aura_buff(player, buff, source_entity)
func _on_role_changed(player: Node, _role_type: int) -> void:
active_auras.erase(player)
var ability_set: AbilitySet = PlayerData.ability_set
if not ability_set:
return
for ability in ability_set.abilities:
if ability and ability.type == Ability.Type.PASSIVE:
var effect := Effect.new()
effect.effect_name = ability.ability_name
effect.type = Effect.Type.AURA
effect.stat = ability.passive_stat
effect.value = ability.damage / 100.0
effect.duration = -1.0
effect.is_multiplier = true
effect.aura_radius = ability.ability_range
if not active_auras.has(player):
active_auras[player] = []
active_auras[player].append(effect)
func _on_entity_died(entity: Node) -> void:
active_auras.erase(entity)

View File

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

142
systems/buff_system.gd Normal file
View File

@@ -0,0 +1,142 @@
extends Node
var active_buffs: Dictionary = {}
func _ready() -> void:
EventBus.effect_requested.connect(_on_effect_requested)
EventBus.role_changed.connect(_on_role_changed)
EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void:
for entity in active_buffs.keys():
if not is_instance_valid(entity):
active_buffs.erase(entity)
continue
var entries: Array = active_buffs[entity]
var i: int = entries.size() - 1
while i >= 0:
var entry: Dictionary = entries[i]
var effect: Effect = entry["effect"]
if effect.duration > 0:
entry["remaining"] -= delta
if entry["remaining"] <= 0:
var is_aura_buff: bool = entry.get("is_aura_buff", false)
entries.remove_at(i)
if not is_aura_buff:
EventBus.effect_expired.emit(entity, effect)
_recalc(entity)
i -= 1
continue
if effect.tick_interval > 0:
entry["tick_timer"] -= delta
if entry["tick_timer"] <= 0:
entry["tick_timer"] += effect.tick_interval
if not effect.is_multiplier and effect.type == Effect.Type.BUFF:
var source: Node = entry["source"]
if not is_instance_valid(source):
source = entity
EventBus.heal_requested.emit(source, entity, effect.value)
i -= 1
func apply(target: Node, effect: Effect, source: Node) -> void:
if effect.type != Effect.Type.BUFF and effect.type != Effect.Type.AURA:
return
if not active_buffs.has(target):
active_buffs[target] = []
var replaced := false
var entries: Array = active_buffs[target]
for i in range(entries.size()):
if entries[i]["effect"].effect_name == effect.effect_name:
entries[i]["effect"] = effect
entries[i]["source"] = source
entries[i]["remaining"] = effect.duration
entries[i]["tick_timer"] = effect.tick_interval
replaced = true
break
if not replaced:
entries.append({
"effect": effect,
"source": source,
"remaining": effect.duration,
"tick_timer": effect.tick_interval,
})
EventBus.effect_applied.emit(target, effect)
if effect.is_multiplier:
_recalc(target)
func apply_aura_buff(target: Node, effect: Effect, source: Node) -> void:
if not active_buffs.has(target):
active_buffs[target] = []
var entry := {
"effect": effect,
"source": source,
"remaining": effect.duration,
"tick_timer": effect.tick_interval,
"aura_source": source,
"is_aura_buff": true,
}
active_buffs[target].append(entry)
if effect.is_multiplier:
_recalc(target)
func has_aura_buff(target: Node, aura_name: String, source: Node) -> bool:
if not active_buffs.has(target):
return false
for entry in active_buffs[target]:
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
return true
return false
func refresh_aura_buff(target: Node, aura_name: String, source: Node, duration: float) -> void:
if not active_buffs.has(target):
return
for entry in active_buffs[target]:
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
entry["remaining"] = duration
return
func clear(entity: Node) -> void:
active_buffs.erase(entity)
_recalc(entity)
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
if effect.type == Effect.Type.BUFF or effect.type == Effect.Type.AURA:
apply(target, effect, source)
func _on_entity_died(entity: Node) -> void:
active_buffs.erase(entity)
_recalc(entity)
func _on_role_changed(player: Node, _role_type: int) -> void:
_remove_permanent(player)
func _remove_permanent(entity: Node) -> void:
if not active_buffs.has(entity):
return
var entries: Array = active_buffs[entity]
var i: int = entries.size() - 1
while i >= 0:
if entries[i]["effect"].duration < 0:
EventBus.effect_expired.emit(entity, entries[i]["effect"])
entries.remove_at(i)
i -= 1
_recalc(entity)
func _recalc(entity: Node) -> void:
var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 }
if active_buffs.has(entity):
for entry in active_buffs[entity]:
var effect: Effect = entry["effect"]
if effect.is_multiplier and effect.stat in mults:
mults[effect.stat] += effect.value
var player: Node = get_tree().get_first_node_in_group("player")
if entity == player:
PlayerData.buff_damage = mults["damage"]
PlayerData.buff_heal = mults["heal"]
PlayerData.buff_shield = mults["shield"]
if PlayerData.base:
var new_max: float = PlayerData.base.max_shield * mults["shield"]
PlayerData.max_shield = new_max
PlayerData.shield = min(PlayerData.shield, new_max)
PlayerData.set_shield(PlayerData.shield)
EventBus.buff_changed.emit(entity, "damage", mults["damage"])

View File

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

View File

@@ -1,73 +1,19 @@
extends Node extends Node
var cooldowns: Dictionary = {}
func _ready() -> void: func _ready() -> void:
add_to_group("cooldown_system")
EventBus.role_changed.connect(_on_role_changed) EventBus.role_changed.connect(_on_role_changed)
func register(entity: Node, ability_count: int) -> void:
cooldowns[entity] = {
"cds": [] as Array[float],
"max_cds": [] as Array[float],
"gcd": 0.0,
"aa": 0.0,
}
cooldowns[entity]["cds"].resize(ability_count)
cooldowns[entity]["cds"].fill(0.0)
cooldowns[entity]["max_cds"].resize(ability_count)
cooldowns[entity]["max_cds"].fill(0.0)
func deregister(entity: Node) -> void:
cooldowns.erase(entity)
func _process(delta: float) -> void: func _process(delta: float) -> void:
for entity in cooldowns: if PlayerData.gcd > 0:
if not is_instance_valid(entity): PlayerData.gcd -= delta
continue if PlayerData.aa_timer > 0:
var data: Dictionary = cooldowns[entity] PlayerData.aa_timer -= delta
if data["gcd"] > 0: for i in range(PlayerData.cooldowns.size()):
data["gcd"] -= delta if PlayerData.cooldowns[i] > 0:
if data["aa"] > 0: PlayerData.cooldowns[i] -= delta
data["aa"] -= delta EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.gcd)
var cds: Array = data["cds"]
for i in range(cds.size()):
if cds[i] > 0:
cds[i] -= delta
EventBus.cooldown_tick.emit(cds, data["max_cds"], data["gcd"])
func is_ready(entity: Node, index: int) -> bool: func _on_role_changed(_player: Node, _role_type: int) -> void:
if entity not in cooldowns: PlayerData.cooldowns.fill(0.0)
return false PlayerData.max_cooldowns.fill(0.0)
return cooldowns[entity]["cds"][index] <= 0 PlayerData.gcd = 0.0
func is_gcd_ready(entity: Node) -> bool:
if entity not in cooldowns:
return false
return cooldowns[entity]["gcd"] <= 0
func is_aa_ready(entity: Node) -> bool:
if entity not in cooldowns:
return false
return cooldowns[entity]["aa"] <= 0
func set_cooldown(entity: Node, index: int, cd: float, gcd: float) -> void:
if entity not in cooldowns:
return
var data: Dictionary = cooldowns[entity]
data["cds"][index] = cd
data["max_cds"][index] = max(cd, gcd)
if gcd > 0:
data["gcd"] = gcd
func set_aa_cooldown(entity: Node, cd: float) -> void:
if entity not in cooldowns:
return
cooldowns[entity]["aa"] = cd
func _on_role_changed(player: Node, _role_type: int) -> void:
if player in cooldowns:
var data: Dictionary = cooldowns[player]
data["cds"].fill(0.0)
data["max_cds"].fill(0.0)
data["gcd"] = 0.0

View File

@@ -1 +1,38 @@
extends Node extends Node
func _ready() -> void:
EventBus.damage_requested.connect(_on_damage_requested)
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
var remaining: float = amount
var shield_system: Node = get_node_or_null("../ShieldSystem")
if shield_system:
remaining = shield_system.absorb(target, remaining)
EventBus.damage_dealt.emit(attacker, target, amount)
if remaining > 0:
_apply_damage(target, remaining)
func _apply_damage(entity: Node, amount: float) -> void:
if entity == _get_player():
var health: float = PlayerData.health - amount
if health < 0:
health = 0
PlayerData.set_health(health)
elif entity.is_in_group("boss"):
var health: float = BossData.get_stat(entity, "health") - amount
if health < 0:
health = 0
BossData.set_health(entity, health)
elif entity.is_in_group("enemies"):
var health: float = EnemyData.get_stat(entity, "health") - amount
if health < 0:
health = 0
EnemyData.set_health(entity, health)
elif entity.is_in_group("portals"):
var health: float = PortalData.get_stat(entity, "health") - amount
if health < 0:
health = 0
PortalData.set_health(entity, health)
func _get_player() -> Node:
return get_tree().get_first_node_in_group("player")

View File

@@ -1 +1 @@
uid://cbd1bryh0e2dw uid://cmy1kqo1pk1q8

65
systems/debuff_system.gd Normal file
View File

@@ -0,0 +1,65 @@
extends Node
var active_debuffs: Dictionary = {}
func _ready() -> void:
EventBus.effect_requested.connect(_on_effect_requested)
EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void:
for entity in active_debuffs.keys():
if not is_instance_valid(entity):
active_debuffs.erase(entity)
continue
var entries: Array = active_debuffs[entity]
var i: int = entries.size() - 1
while i >= 0:
var entry: Dictionary = entries[i]
var effect: Effect = entry["effect"]
if effect.duration > 0:
entry["remaining"] -= delta
if entry["remaining"] <= 0:
entries.remove_at(i)
EventBus.effect_expired.emit(entity, effect)
i -= 1
continue
if effect.tick_interval > 0:
entry["tick_timer"] -= delta
if entry["tick_timer"] <= 0:
entry["tick_timer"] += effect.tick_interval
var source: Node = entry["source"]
if not is_instance_valid(source):
source = entity
EventBus.damage_requested.emit(source, entity, effect.value)
i -= 1
func apply(target: Node, effect: Effect, source: Node) -> void:
if effect.type != Effect.Type.DEBUFF:
return
if not active_debuffs.has(target):
active_debuffs[target] = []
var replaced := false
var entries: Array = active_debuffs[target]
for i in range(entries.size()):
if entries[i]["effect"].effect_name == effect.effect_name:
entries[i]["effect"] = effect
entries[i]["source"] = source
entries[i]["remaining"] = effect.duration
entries[i]["tick_timer"] = effect.tick_interval
replaced = true
break
if not replaced:
entries.append({
"effect": effect,
"source": source,
"remaining": effect.duration,
"tick_timer": effect.tick_interval,
})
EventBus.effect_applied.emit(target, effect)
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
if effect.type == Effect.Type.DEBUFF:
apply(target, effect, source)
func _on_entity_died(entity: Node) -> void:
active_debuffs.erase(entity)

View File

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

View File

@@ -1,16 +1,13 @@
extends Node extends Node
func _ready() -> void: func _ready() -> void:
var player: Node = get_tree().get_first_node_in_group("player")
if player:
GameState.restore_player(player)
EventBus.entity_died.connect(_on_entity_died) EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void: func _on_entity_died(entity: Node) -> void:
if entity.is_in_group("boss"): if entity.is_in_group("boss"):
await get_tree().create_timer(2.0).timeout await get_tree().create_timer(2.0).timeout
GameState.dungeon_cleared = true PlayerData.dungeon_cleared = true
GameState.returning_from_dungeon = false PlayerData.returning_from_dungeon = false
GameState.clear() PlayerData.clear_cache()
EventBus.dungeon_cleared.emit() EventBus.dungeon_cleared.emit()
get_tree().change_scene_to_file("res://scenes/world/world.tscn") get_tree().change_scene_to_file("res://scenes/world/world.tscn")

View File

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

View File

@@ -1,190 +0,0 @@
extends Node
var active_effects: Dictionary = {}
func _ready() -> void:
EventBus.role_changed.connect(_on_role_changed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.effect_requested.connect(_on_effect_requested)
const AURA_REFRESH := 0.5
func _process(delta: float) -> void:
for entity in active_effects.keys():
if not is_instance_valid(entity):
active_effects.erase(entity)
continue
var entries: Array = active_effects[entity]
var i: int = entries.size() - 1
while i >= 0:
var entry: Dictionary = entries[i]
var effect: Effect = entry["effect"]
if effect.duration > 0:
entry["remaining"] -= delta
if entry["remaining"] <= 0:
var is_aura_buff: bool = entry.get("is_aura_buff", false)
entries.remove_at(i)
if not is_aura_buff:
EventBus.effect_expired.emit(entity, effect)
_recalc_stat_buffs(entity)
i -= 1
continue
if effect.tick_interval > 0:
entry["tick_timer"] -= delta
if entry["tick_timer"] <= 0:
entry["tick_timer"] += effect.tick_interval
_apply_tick(entity, entry)
if effect.type == Effect.Type.AURA and effect.aura_radius > 0 and effect.duration < 0:
_propagate_aura(entity, entry, effect)
i -= 1
func _propagate_aura(source_entity: Node, _entry: Dictionary, aura: Effect) -> void:
if not source_entity is Node3D:
return
var players := get_tree().get_nodes_in_group("player")
for player in players:
if not is_instance_valid(player) or not Stats.is_alive(player):
continue
var dist: float = source_entity.global_position.distance_to(player.global_position)
if dist > aura.aura_radius:
continue
if _has_aura_buff(player, aura.effect_name, source_entity):
_refresh_aura_buff(player, aura.effect_name, source_entity)
else:
var buff := Effect.new()
buff.effect_name = aura.effect_name
buff.type = Effect.Type.BUFF
buff.stat = aura.stat
buff.value = aura.value
buff.duration = AURA_REFRESH
buff.is_multiplier = aura.is_multiplier
_apply_aura_buff(player, buff, source_entity)
func _has_aura_buff(target: Node, aura_name: String, source: Node) -> bool:
if not active_effects.has(target):
return false
for entry in active_effects[target]:
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
return true
return false
func _refresh_aura_buff(target: Node, aura_name: String, source: Node) -> void:
if not active_effects.has(target):
return
for entry in active_effects[target]:
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
entry["remaining"] = AURA_REFRESH
return
func _apply_aura_buff(target: Node, effect: Effect, source: Node) -> void:
if not active_effects.has(target):
active_effects[target] = []
var entry := {
"effect": effect,
"source": source,
"remaining": effect.duration,
"tick_timer": effect.tick_interval,
"aura_source": source,
"is_aura_buff": true,
}
active_effects[target].append(entry)
if effect.is_multiplier:
_recalc_stat_buffs(target)
func apply_effect(target: Node, effect: Effect, source: Node) -> void:
if not active_effects.has(target):
active_effects[target] = []
var replaced := false
var entries: Array = active_effects[target]
for i in range(entries.size()):
if entries[i]["effect"].effect_name == effect.effect_name:
entries[i]["effect"] = effect
entries[i]["source"] = source
entries[i]["remaining"] = effect.duration
entries[i]["tick_timer"] = effect.tick_interval
replaced = true
break
if not replaced:
entries.append({
"effect": effect,
"source": source,
"remaining": effect.duration,
"tick_timer": effect.tick_interval,
})
EventBus.effect_applied.emit(target, effect)
if effect.is_multiplier:
_recalc_stat_buffs(target)
func clear_effects(entity: Node) -> void:
active_effects.erase(entity)
if is_instance_valid(entity):
_recalc_stat_buffs(entity)
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
apply_effect(target, effect, source)
func _on_entity_died(entity: Node) -> void:
clear_effects(entity)
func _on_role_changed(player: Node, _role_type: int) -> void:
_remove_permanent_effects(player)
var role: Node = player.get_node_or_null("Role")
if not role:
return
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
return
for ability in ability_set.abilities:
if ability and ability.type == Ability.Type.PASSIVE:
var effect := Effect.new()
effect.effect_name = ability.ability_name
effect.type = Effect.Type.AURA
effect.stat = ability.passive_stat
effect.value = ability.damage / 100.0
effect.duration = -1.0
effect.is_multiplier = true
effect.aura_radius = ability.ability_range
apply_effect(player, effect, player)
func _remove_permanent_effects(entity: Node) -> void:
if not active_effects.has(entity):
return
var entries: Array = active_effects[entity]
var i: int = entries.size() - 1
while i >= 0:
if entries[i]["effect"].duration < 0:
EventBus.effect_expired.emit(entity, entries[i]["effect"])
entries.remove_at(i)
i -= 1
_recalc_stat_buffs(entity)
func _recalc_stat_buffs(entity: Node) -> void:
var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 }
if active_effects.has(entity):
for entry in active_effects[entity]:
var effect: Effect = entry["effect"]
if effect.is_multiplier and effect.stat in mults:
mults[effect.stat] += effect.value
for stat in mults:
Stats.set_stat(entity, "buff_" + stat, mults[stat])
EventBus.buff_changed.emit(entity, stat, mults[stat])
var base: BaseStats = Stats.get_base(entity)
if base:
var shield_mult: float = mults["shield"]
var new_max: float = base.max_shield * shield_mult
Stats.set_stat(entity, "max_shield", new_max)
var shield: float = Stats.get_stat(entity, "shield")
shield = min(shield, new_max)
Stats.set_stat(entity, "shield", shield)
EventBus.shield_changed.emit(entity, shield, new_max)
func _apply_tick(entity: Node, entry: Dictionary) -> void:
var effect: Effect = entry["effect"]
var source: Node = entry["source"]
if not is_instance_valid(source):
source = entity
if not effect.is_multiplier:
if effect.type == Effect.Type.DEBUFF:
EventBus.damage_requested.emit(source, entity, effect.value)
elif effect.type == Effect.Type.BUFF:
EventBus.heal_requested.emit(source, entity, effect.value)

View File

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

View File

@@ -1,36 +0,0 @@
extends Node
var attack_timers: Dictionary = {}
func _physics_process(delta: float) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy) or not Stats.is_alive(enemy):
continue
if enemy.state != enemy.State.ATTACK:
continue
_handle_attack(enemy, delta)
func _handle_attack(enemy: Node, delta: float) -> void:
if enemy not in attack_timers:
attack_timers[enemy] = 0.0
attack_timers[enemy] -= delta
if not is_instance_valid(enemy.target):
enemy.state = enemy.State.RETURN
return
var base: BaseStats = Stats.get_base(enemy)
var attack_range: float = base.attack_range if base is EnemyStats else 2.0
var dist: float = enemy.global_position.distance_to(enemy.target.global_position)
if dist > attack_range:
enemy.state = enemy.State.CHASE
return
if attack_timers[enemy] <= 0:
var attack_cooldown: float = base.attack_cooldown if base is EnemyStats else 1.5
var attack_damage: float = base.attack_damage if base is EnemyStats else 5.0
attack_timers[enemy] = attack_cooldown
EventBus.damage_requested.emit(enemy, enemy.target, attack_damage)
enemy.velocity.x = 0
enemy.velocity.z = 0

View File

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

22
systems/heal_system.gd Normal file
View File

@@ -0,0 +1,22 @@
extends Node
func _ready() -> void:
EventBus.heal_requested.connect(_on_heal_requested)
func _on_heal_requested(_healer: Node, target: Node, amount: float) -> void:
if target == _get_player():
var health: float = min(PlayerData.health + amount, PlayerData.max_health)
PlayerData.set_health(health)
elif target.is_in_group("boss"):
var health: float = BossData.get_stat(target, "health")
var max_health: float = BossData.get_stat(target, "max_health")
health = min(health + amount, max_health)
BossData.set_health(target, health)
elif target.is_in_group("enemies"):
var health: float = EnemyData.get_stat(target, "health")
var max_health: float = EnemyData.get_stat(target, "max_health")
health = min(health + amount, max_health)
EnemyData.set_health(target, health)
func _get_player() -> Node:
return get_tree().get_first_node_in_group("player")

View File

@@ -0,0 +1 @@
uid://8jyik37e4tjw

View File

@@ -1,14 +1,25 @@
extends Node extends Node
func _ready() -> void: func _ready() -> void:
EventBus.damage_requested.connect(_on_damage_requested) _emit_initial.call_deferred()
EventBus.heal_requested.connect(_on_heal_requested)
func _process(delta: float) -> void: func _process(delta: float) -> void:
for entity in Stats.entities: _regen_player(delta)
_regen_entities(delta, EnemyData.entities)
_regen_entities(delta, BossData.entities)
func _regen_player(delta: float) -> void:
if not PlayerData.alive or PlayerData.health_regen <= 0:
return
if PlayerData.health < PlayerData.max_health:
var health: float = min(PlayerData.health + PlayerData.health_regen * delta, PlayerData.max_health)
PlayerData.set_health(health)
func _regen_entities(delta: float, entities: Dictionary) -> void:
for entity in entities:
if not is_instance_valid(entity): if not is_instance_valid(entity):
continue continue
var data: Dictionary = Stats.entities[entity] var data: Dictionary = entities[entity]
if not data["alive"]: if not data["alive"]:
continue continue
var regen: float = data["health_regen"] var regen: float = data["health_regen"]
@@ -16,34 +27,6 @@ func _process(delta: float) -> void:
data["health"] = min(data["health"] + regen * delta, data["max_health"]) data["health"] = min(data["health"] + regen * delta, data["max_health"])
EventBus.health_changed.emit(entity, data["health"], data["max_health"]) EventBus.health_changed.emit(entity, data["health"], data["max_health"])
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: func _emit_initial() -> void:
if not Stats.is_alive(target): EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health)
return EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield)
var remaining: float = amount
var shield_system: Node = get_node_or_null("../ShieldSystem")
if shield_system:
remaining = shield_system.absorb(target, remaining)
EventBus.damage_dealt.emit(attacker, target, amount)
if remaining > 0:
_take_damage(target, remaining)
func _take_damage(entity: Node, amount: float) -> void:
var health: float = Stats.get_stat(entity, "health")
health -= amount
if health <= 0:
health = 0
Stats.set_stat(entity, "health", health)
var max_health: float = Stats.get_stat(entity, "max_health")
EventBus.health_changed.emit(entity, health, max_health)
if health <= 0:
Stats.set_stat(entity, "alive", false)
EventBus.entity_died.emit(entity)
func _on_heal_requested(healer: Node, target: Node, amount: float) -> void:
if not Stats.is_alive(target):
return
var health: float = Stats.get_stat(target, "health")
var max_health: float = Stats.get_stat(target, "max_health")
health = min(health + amount, max_health)
Stats.set_stat(target, "health", health)
EventBus.health_changed.emit(target, health, max_health)

View File

@@ -1 +1 @@
uid://b3wkn5118dimy uid://h362ftxb0cns

View File

@@ -1,75 +1,105 @@
extends CanvasLayer extends Node
const GCD_TIME := 0.5 const GCD_TIME := 0.5
const ICON_SIZE := 20
@onready var health_bar: ProgressBar = $HealthBar const FONT_SIZE := 14
@onready var health_label: Label = $HealthBar/HealthLabel const BORDER_WIDTH := 2
@onready var shield_bar: ProgressBar = $ShieldBar const MARGIN := 2
@onready var shield_label: Label = $ShieldBar/ShieldLabel
@onready var respawn_label: Label = $RespawnTimer
@onready var class_icon: Label = $AbilityBar/ClassIcon/Label
@onready var ability_panels: Array = [
$AbilityBar/Ability1,
$AbilityBar/Ability2,
$AbilityBar/Ability3,
$AbilityBar/Ability4,
$AbilityBar/Ability5,
]
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"] var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
var effect_container: HBoxContainer = null var effect_container: HBoxContainer = null
func _ready() -> void: func _ready() -> void:
respawn_label.visible = false
_create_effect_container()
EventBus.health_changed.connect(_on_health_changed) EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed) EventBus.shield_changed.connect(_on_shield_changed)
EventBus.entity_died.connect(_on_entity_died) EventBus.entity_died.connect(_on_entity_died)
EventBus.player_respawned.connect(_on_player_respawned) EventBus.player_respawned.connect(_on_player_respawned)
EventBus.role_changed.connect(_on_role_changed)
EventBus.respawn_tick.connect(_on_respawn_tick) EventBus.respawn_tick.connect(_on_respawn_tick)
EventBus.role_changed.connect(_on_role_changed)
EventBus.cooldown_tick.connect(_on_cooldown_tick) EventBus.cooldown_tick.connect(_on_cooldown_tick)
EventBus.effect_applied.connect(_on_effect_applied) EventBus.effect_applied.connect(_on_effect_applied)
EventBus.effect_expired.connect(_on_effect_expired) EventBus.effect_expired.connect(_on_effect_expired)
_init_hud.call_deferred()
func _init_hud() -> void:
var hud: CanvasLayer = _get_hud()
if not hud:
return
hud.get_node("RespawnTimer").visible = false
effect_container = HBoxContainer.new()
effect_container.name = "EffectContainer"
effect_container.position = Vector2(10, 60)
effect_container.add_theme_constant_override("separation", 3)
hud.add_child(effect_container)
func _on_health_changed(entity: Node, current: float, max_val: float) -> void: func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player": if entity != PlayerData:
health_bar.max_value = max_val return
health_bar.value = current var hud: CanvasLayer = _get_hud()
health_label.text = "%d/%d" % [current, max_val] if not hud:
return
var bar: ProgressBar = hud.get_node("HealthBar")
bar.max_value = max_val
bar.value = current
hud.get_node("HealthBar/HealthLabel").text = "%d/%d" % [current, max_val]
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player": if entity != PlayerData:
shield_bar.max_value = max_val return
shield_bar.value = current var hud: CanvasLayer = _get_hud()
shield_label.text = "%d/%d" % [current, max_val] if not hud:
return
var bar: ProgressBar = hud.get_node("ShieldBar")
bar.max_value = max_val
bar.value = current
hud.get_node("ShieldBar/ShieldLabel").text = "%d/%d" % [current, max_val]
func _on_entity_died(entity: Node) -> void: func _on_entity_died(entity: Node) -> void:
if entity.name == "Player": if entity != PlayerData:
respawn_label.visible = true return
var hud: CanvasLayer = _get_hud()
if hud:
hud.get_node("RespawnTimer").visible = true
func _on_player_respawned(_player: Node) -> void: func _on_player_respawned(_player: Node) -> void:
respawn_label.visible = false var hud: CanvasLayer = _get_hud()
if hud:
hud.get_node("RespawnTimer").visible = false
func _on_respawn_tick(timer: float) -> void: func _on_respawn_tick(timer: float) -> void:
respawn_label.text = str(ceil(timer)) var hud: CanvasLayer = _get_hud()
if hud:
hud.get_node("RespawnTimer").text = str(ceil(timer))
func _on_role_changed(_player: Node, role_type: int) -> void: func _on_role_changed(_player: Node, role_type: int) -> void:
var hud: CanvasLayer = _get_hud()
if not hud:
return
var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label")
match role_type: match role_type:
0: class_icon.text = "T" 0: icon.text = "T"
1: class_icon.text = "D" 1: icon.text = "D"
2: class_icon.text = "H" 2: icon.text = "H"
func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void: func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void:
for i in range(min(ability_panels.size(), cooldowns.size())): var hud: CanvasLayer = _get_hud()
var panel: Panel = ability_panels[i] if not hud:
return
var panels: Array = [
hud.get_node("AbilityBar/Ability1"),
hud.get_node("AbilityBar/Ability2"),
hud.get_node("AbilityBar/Ability3"),
hud.get_node("AbilityBar/Ability4"),
hud.get_node("AbilityBar/Ability5"),
]
for i in range(min(panels.size(), cooldowns.size())):
var panel: Panel = panels[i]
var label: Label = panel.get_node("Label") var label: Label = panel.get_node("Label")
var overlay: ColorRect = panel.get_node("CooldownOverlay") var overlay: ColorRect = panel.get_node("CooldownOverlay")
var cd: float = cooldowns[i] var cd: float = cooldowns[i]
var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0 var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0
var active_cd: float = max(cd, gcd) var active_cd: float = max(cd, gcd)
var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME
if active_cd > 0: if active_cd > 0:
var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0) var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0)
overlay.visible = true overlay.visible = true
@@ -79,43 +109,19 @@ func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float)
overlay.visible = false overlay.visible = false
label.text = ability_labels[i] label.text = ability_labels[i]
func _create_effect_container() -> void:
effect_container = HBoxContainer.new()
effect_container.name = "EffectContainer"
effect_container.position = Vector2(10, 60)
effect_container.add_theme_constant_override("separation", 3)
add_child(effect_container)
func _on_effect_applied(target: Node, effect: Effect) -> void: func _on_effect_applied(target: Node, effect: Effect) -> void:
if target.name != "Player": if target != PlayerData:
return return
var panel := _create_icon_panel(effect) if effect_container:
var insert_idx: int = _get_sorted_index(effect.type) _add_icon(effect)
effect_container.add_child(panel)
effect_container.move_child(panel, insert_idx)
func _on_effect_expired(target: Node, effect: Effect) -> void: func _on_effect_expired(target: Node, effect: Effect) -> void:
if target.name != "Player": if target != PlayerData:
return return
for child in effect_container.get_children(): if effect_container:
if child.has_meta("effect_type") and child.has_meta("effect_name"): _remove_icon(effect)
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
child.queue_free()
return
func _get_sorted_index(type: int) -> int: func _add_icon(effect: Effect) -> void:
var idx := 0
for child in effect_container.get_children():
if not child.has_meta("effect_type"):
continue
var child_type: int = child.get_meta("effect_type")
if child_type <= type:
idx += 1
else:
break
return idx
func _create_icon_panel(effect: Effect) -> PanelContainer:
var panel := PanelContainer.new() var panel := PanelContainer.new()
var style := StyleBoxFlat.new() var style := StyleBoxFlat.new()
match effect.type: match effect.type:
@@ -128,18 +134,35 @@ func _create_icon_panel(effect: Effect) -> PanelContainer:
Effect.Type.DEBUFF: Effect.Type.DEBUFF:
style.bg_color = Color(0.3, 0.15, 0.15, 1) style.bg_color = Color(0.3, 0.15, 0.15, 1)
style.border_color = Color(1.0, 0.3, 0.3, 1) style.border_color = Color(1.0, 0.3, 0.3, 1)
style.set_border_width_all(2) style.set_border_width_all(BORDER_WIDTH)
style.set_content_margin_all(2) style.set_content_margin_all(MARGIN)
panel.add_theme_stylebox_override("panel", style) panel.add_theme_stylebox_override("panel", style)
var label := Label.new() var label := Label.new()
label.text = effect.effect_name.left(1) label.text = effect.effect_name.left(1)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 14) label.add_theme_font_size_override("font_size", FONT_SIZE)
label.add_theme_color_override("font_color", Color.WHITE) label.add_theme_color_override("font_color", Color.WHITE)
label.custom_minimum_size = Vector2(20, 20) label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
panel.add_child(label) panel.add_child(label)
panel.custom_minimum_size = Vector2(24, 24) panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2)
panel.set_meta("effect_type", effect.type) panel.set_meta("effect_type", effect.type)
panel.set_meta("effect_name", effect.effect_name) panel.set_meta("effect_name", effect.effect_name)
return panel var insert_idx := 0
for child in effect_container.get_children():
if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type:
insert_idx += 1
else:
break
effect_container.add_child(panel)
effect_container.move_child(panel, insert_idx)
func _remove_icon(effect: Effect) -> void:
for child in effect_container.get_children():
if child.has_meta("effect_type") and child.has_meta("effect_name"):
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
child.queue_free()
return
func _get_hud() -> CanvasLayer:
return get_tree().get_first_node_in_group("hud") as CanvasLayer

View File

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

225
systems/nameplate_system.gd Normal file
View File

@@ -0,0 +1,225 @@
extends Node
const ICON_SIZE := 10
const FONT_SIZE := 7
const BORDER_WIDTH := 1
const ICON_MARGIN := 0
const BASE_HEIGHT := 29
var styles: Dictionary = {}
func _ready() -> void:
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
EventBus.target_changed.connect(_on_target_changed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.effect_applied.connect(_on_effect_applied)
EventBus.effect_expired.connect(_on_effect_expired)
EventBus.portal_spawn.connect(_on_portal_spawn)
_init_nameplates.call_deferred()
func _init_nameplates() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
_setup_nameplate(enemy)
for portal in get_tree().get_nodes_in_group("portals"):
_setup_nameplate(portal)
func _setup_nameplate(entity: Node) -> void:
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
if not nameplate:
return
var viewport: SubViewport = nameplate.get_node("SubViewport")
nameplate.texture = viewport.get_texture()
var border: ColorRect = viewport.get_node_or_null("Border")
if border:
border.visible = false
func _process(_delta: float) -> void:
var player: Node = get_tree().get_first_node_in_group("player")
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar")
if not nameplate:
continue
var health_bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar")
var data_source: Node = _get_data_source(enemy)
if not data_source:
continue
if enemy not in styles:
var style_normal: StyleBoxFlat = health_bar.get_theme_stylebox("fill").duplicate()
var style_aggro: StyleBoxFlat = style_normal.duplicate()
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
styles[enemy] = { "normal": style_normal, "aggro": style_aggro }
var s: Dictionary = styles[enemy]
var enemy_target: Variant = data_source.get_stat(enemy, "target")
if player and enemy_target == player:
health_bar.add_theme_stylebox_override("fill", s["aggro"])
else:
health_bar.add_theme_stylebox_override("fill", s["normal"])
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity == PlayerData:
return
if not is_instance_valid(entity):
return
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
if not nameplate:
return
if not nameplate.texture:
_setup_nameplate(entity)
var bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar")
bar.max_value = max_val
bar.value = current
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity == PlayerData:
return
if not is_instance_valid(entity):
return
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
if not nameplate:
return
var bar: ProgressBar = nameplate.get_node_or_null("SubViewport/ShieldBar")
if not bar:
return
if max_val <= 0:
bar.visible = false
return
bar.visible = true
bar.max_value = max_val
bar.value = current
func _on_target_changed(_player: Node, target: Node) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar")
if nameplate:
nameplate.get_node("SubViewport/Border").visible = (target == enemy)
for portal in get_tree().get_nodes_in_group("portals"):
if not is_instance_valid(portal):
continue
var nameplate: Sprite3D = portal.get_node_or_null("Healthbar")
if nameplate:
nameplate.get_node("SubViewport/Border").visible = (target == portal)
func _on_entity_died(entity: Node) -> void:
if entity != PlayerData and is_instance_valid(entity):
styles.erase(entity)
func _on_effect_applied(target: Node, effect: Effect) -> void:
if target == PlayerData:
return
if not is_instance_valid(target):
return
var nameplate: Sprite3D = target.get_node_or_null("Healthbar")
if not nameplate:
return
var container: HBoxContainer = _get_or_create_effect_container(nameplate)
_add_icon(container, effect)
_resize_viewport(nameplate)
func _on_effect_expired(target: Node, effect: Effect) -> void:
if target == PlayerData:
return
if not is_instance_valid(target):
return
var nameplate: Sprite3D = target.get_node_or_null("Healthbar")
if not nameplate:
return
var container: HBoxContainer = nameplate.get_node_or_null("SubViewport/EffectContainer")
if container:
_remove_icon(container, effect)
_resize_viewport.call_deferred(nameplate)
func _on_portal_spawn(_portal: Node, enemies: Array) -> void:
for enemy in enemies:
_setup_nameplate.call_deferred(enemy)
func _get_or_create_effect_container(nameplate: Sprite3D) -> HBoxContainer:
var viewport: SubViewport = nameplate.get_node("SubViewport")
var container: HBoxContainer = viewport.get_node_or_null("EffectContainer")
if container:
return container
container = HBoxContainer.new()
container.name = "EffectContainer"
var health_bar: ProgressBar = viewport.get_node("HealthBar")
var shield_bar: ProgressBar = viewport.get_node_or_null("ShieldBar")
var y_pos: float = 0.0
if shield_bar and shield_bar.visible:
y_pos = shield_bar.offset_bottom + 2
else:
y_pos = health_bar.offset_bottom + 2
container.position = Vector2(2, y_pos)
container.add_theme_constant_override("separation", 1)
viewport.add_child(container)
return container
func _resize_viewport(nameplate: Sprite3D) -> void:
var viewport: SubViewport = nameplate.get_node("SubViewport")
var border: ColorRect = viewport.get_node("Border")
var container: HBoxContainer = viewport.get_node_or_null("EffectContainer")
if not container:
return
var icon_count := 0
for child in container.get_children():
if not child.is_queued_for_deletion():
icon_count += 1
if icon_count > 0:
var needed: int = int(container.position.y) + ICON_SIZE + 4
viewport.size.y = max(BASE_HEIGHT, needed)
border.offset_bottom = viewport.size.y
else:
viewport.size.y = BASE_HEIGHT
border.offset_bottom = BASE_HEIGHT
func _add_icon(container: HBoxContainer, effect: Effect) -> void:
var panel := PanelContainer.new()
var style := StyleBoxFlat.new()
match effect.type:
Effect.Type.AURA:
style.bg_color = Color(0.15, 0.15, 0.3, 1)
style.border_color = Color(0.3, 0.5, 1.0, 1)
Effect.Type.BUFF:
style.bg_color = Color(0.15, 0.3, 0.15, 1)
style.border_color = Color(0.3, 1.0, 0.3, 1)
Effect.Type.DEBUFF:
style.bg_color = Color(0.3, 0.15, 0.15, 1)
style.border_color = Color(1.0, 0.3, 0.3, 1)
style.set_border_width_all(BORDER_WIDTH)
style.set_content_margin_all(ICON_MARGIN)
panel.add_theme_stylebox_override("panel", style)
var label := Label.new()
label.text = effect.effect_name.left(1)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", FONT_SIZE)
label.add_theme_color_override("font_color", Color.WHITE)
label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
panel.add_child(label)
panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2)
panel.set_meta("effect_type", effect.type)
panel.set_meta("effect_name", effect.effect_name)
var insert_idx := 0
for child in container.get_children():
if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type:
insert_idx += 1
else:
break
container.add_child(panel)
container.move_child(panel, insert_idx)
func _remove_icon(container: HBoxContainer, effect: Effect) -> void:
for child in container.get_children():
if child.has_meta("effect_type") and child.has_meta("effect_name"):
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
child.queue_free()
return
func _get_data_source(entity: Node) -> Node:
if entity.is_in_group("boss"):
return BossData
elif entity.is_in_group("enemies"):
return EnemyData
return null

View File

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

21
systems/portal_system.gd Normal file
View File

@@ -0,0 +1,21 @@
extends Node
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
func _ready() -> void:
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if not entity.is_in_group("portals"):
return
if not entity.is_inside_tree():
return
var pos: Vector3 = entity.global_position
var gate: Node3D = GATE_SCENE.instantiate()
entity.get_parent().add_child(gate)
gate.global_position = pos
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy):
enemy.queue_free()
EventBus.portal_defeated.emit(entity)
entity.queue_free()

View File

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

View File

@@ -1,48 +1,42 @@
extends Node extends Node
var dead_players: Dictionary = {} var respawn_timer := 0.0
var is_dead := false
func _ready() -> void: func _ready() -> void:
EventBus.entity_died.connect(_on_entity_died) EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void: func _process(delta: float) -> void:
for player in dead_players.keys(): if not is_dead:
if not is_instance_valid(player): return
dead_players.erase(player) respawn_timer -= delta
continue EventBus.respawn_tick.emit(respawn_timer)
dead_players[player] -= delta if respawn_timer <= 0:
EventBus.respawn_tick.emit(dead_players[player]) _respawn()
if dead_players[player] <= 0:
_respawn(player)
func _on_entity_died(entity: Node) -> void: func _on_entity_died(entity: Node) -> void:
if not entity.is_in_group("player"): if not entity.is_in_group("player"):
return return
if entity in dead_players: if is_dead:
return return
var base: BaseStats = Stats.get_base(entity) is_dead = true
var respawn_time: float = base.respawn_time if base is PlayerStats else 3.0 respawn_timer = PlayerData.respawn_time
dead_players[entity] = respawn_time
entity.velocity = Vector3.ZERO entity.velocity = Vector3.ZERO
entity.get_node("Mesh").visible = false entity.get_node("Mesh").visible = false
entity.get_node("CollisionShape3D").disabled = true entity.get_node("CollisionShape3D").disabled = true
entity.get_node("Movement").set_physics_process(false) entity.get_node("Movement").set_physics_process(false)
entity.get_node("Combat").set_process_unhandled_input(false) entity.get_node("Ability").set_process_unhandled_input(false)
entity.get_node("Targeting").set_process_unhandled_input(false) entity.get_node("Targeting").set_process_unhandled_input(false)
func _respawn(player: Node) -> void: func _respawn() -> void:
dead_players.erase(player) is_dead = false
var player: Node = get_tree().get_first_node_in_group("player")
if not player:
return
player.global_position = Vector3(0, 1, -5) player.global_position = Vector3(0, 1, -5)
player.get_node("Mesh").visible = true player.get_node("Mesh").visible = true
player.get_node("CollisionShape3D").disabled = false player.get_node("CollisionShape3D").disabled = false
player.get_node("Movement").set_physics_process(true) player.get_node("Movement").set_physics_process(true)
player.get_node("Combat").set_process_unhandled_input(true) player.get_node("Ability").set_process_unhandled_input(true)
player.get_node("Targeting").set_process_unhandled_input(true) player.get_node("Targeting").set_process_unhandled_input(true)
var max_health: float = Stats.get_stat(player, "max_health") PlayerData.respawn()
var max_shield: float = Stats.get_stat(player, "max_shield")
Stats.set_stat(player, "health", max_health)
Stats.set_stat(player, "shield", max_shield)
Stats.set_stat(player, "alive", true)
EventBus.health_changed.emit(player, max_health, max_health)
EventBus.shield_changed.emit(player, max_shield, max_shield)
EventBus.player_respawned.emit(player)

23
systems/role_system.gd Normal file
View File

@@ -0,0 +1,23 @@
extends Node
@export var tank_set: AbilitySet
@export var damage_set: AbilitySet
@export var healer_set: AbilitySet
func _ready() -> void:
EventBus.role_change_requested.connect(_on_role_change_requested)
_apply_role.call_deferred(PlayerData.current_role)
func _on_role_change_requested(_player: Node, role: int) -> void:
_apply_role(role)
func _apply_role(role: int) -> void:
PlayerData.current_role = role
match role:
PlayerData.Role.TANK:
PlayerData.ability_set = tank_set
PlayerData.Role.DAMAGE:
PlayerData.ability_set = damage_set
PlayerData.Role.HEALER:
PlayerData.ability_set = healer_set
PlayerData.set_role(role)

View File

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

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