update
This commit is contained in:
14
CLAUDE.md
14
CLAUDE.md
@@ -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.
|
||||
- `scenes/` — Darstellung + Input
|
||||
- `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
|
||||
- `dungeon/` — Dungeon + dungeon_manager
|
||||
- `hud/` — HUD
|
||||
- `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
|
||||
- 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)
|
||||
- `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config
|
||||
- `autoloads/` — Globaler Zustand
|
||||
@@ -44,13 +45,6 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
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%)
|
||||
|
||||
64
autoloads/boss_stats.gd
Normal file
64
autoloads/boss_stats.gd
Normal 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)
|
||||
1
autoloads/boss_stats.gd.uid
Normal file
1
autoloads/boss_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dbr02t7pt4vcn
|
||||
64
autoloads/enemy_stats.gd
Normal file
64
autoloads/enemy_stats.gd
Normal 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)
|
||||
1
autoloads/enemy_stats.gd.uid
Normal file
1
autoloads/enemy_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvxn6y15tvidu
|
||||
@@ -1,8 +1,12 @@
|
||||
extends Node
|
||||
|
||||
# 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_lost(enemy, player)
|
||||
signal portal_entered(portal, player)
|
||||
|
||||
# Kampf
|
||||
signal attack_executed(attacker, position, direction, damage)
|
||||
@@ -29,7 +33,6 @@ signal buff_changed(entity, stat, value)
|
||||
|
||||
# Gegner
|
||||
signal enemy_engaged(enemy, target)
|
||||
signal enemy_lost(enemy, player)
|
||||
|
||||
# Portal
|
||||
signal portal_spawn(portal, enemies)
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://cp2vadwcd12sm
|
||||
145
autoloads/player_stats.gd
Normal file
145
autoloads/player_stats.gd
Normal 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()
|
||||
1
autoloads/player_stats.gd.uid
Normal file
1
autoloads/player_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://blmuqkl3aro5w
|
||||
45
autoloads/portal_stats.gd
Normal file
45
autoloads/portal_stats.gd
Normal 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)
|
||||
1
autoloads/portal_stats.gd.uid
Normal file
1
autoloads/portal_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://doullpjapcsk1
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://cyxmpeib7pcw7
|
||||
26
comminication.md
Normal file
26
comminication.md
Normal 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
63
features.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 --> ...
|
||||
@@ -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
|
||||
@@ -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
58
plan.md
@@ -4,21 +4,27 @@
|
||||
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
|
||||
```
|
||||
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
|
||||
role/ — Rollenwechsel + Ability/AbilitySet-Klassen
|
||||
damage/ — set.tres + abilities/
|
||||
tank/ — 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
|
||||
dungeon/ — Dungeon + dungeon_manager
|
||||
hud/ — HUD
|
||||
hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects)
|
||||
world/ — Hauptszene + portal_spawner
|
||||
systems/ — Spiellogik
|
||||
aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config
|
||||
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
|
||||
event_bus.gd
|
||||
game_state.gd
|
||||
@@ -27,7 +33,7 @@ autoloads/ — Globaler Zustand
|
||||
|
||||
## Szenenbaum
|
||||
- Welt
|
||||
- Systems (11 Systeme als Child-Nodes)
|
||||
- Systems (12 Systeme als Child-Nodes)
|
||||
- Taverne
|
||||
- Player
|
||||
- Portale (dynamisch)
|
||||
@@ -136,20 +142,26 @@ autoloads/ — Globaler Zustand
|
||||
- Listener: entity_died
|
||||
- Event: respawn_tick, player_respawned
|
||||
### 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
|
||||
- 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)
|
||||
- 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
|
||||
- State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer)
|
||||
- Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt
|
||||
- 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, Aura-Propagierung
|
||||
- _recalc_stat_buffs: Multiplier-Effekte aggregieren → buff_damage/heal/shield in Stats
|
||||
- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks
|
||||
- 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)
|
||||
- Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus
|
||||
- Element Enum: NONE, FIRE (erweiterbar)
|
||||
@@ -164,8 +176,6 @@ autoloads/ — Globaler Zustand
|
||||
- Cooldown-Tracking, GCD, AA-Timer per Entity
|
||||
- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem
|
||||
- Event: cooldown_tick
|
||||
### DamageSystem (damage_system.gd)
|
||||
- Reserviert für spätere Schadensberechnung (aktuell leer)
|
||||
### AggroSystem (systems/aggro/)
|
||||
- Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres)
|
||||
- aggro_system.gd — Parent, Config halten, Children verdrahten
|
||||
@@ -209,8 +219,9 @@ autoloads/ — Globaler Zustand
|
||||
- Camera3D
|
||||
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
|
||||
- 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)
|
||||
- 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
|
||||
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung
|
||||
|
||||
@@ -223,7 +234,10 @@ autoloads/ — Globaler Zustand
|
||||
- DetectionArea (Area3D, emittiert enemy_detected)
|
||||
- NavigationAgent3D
|
||||
- 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
|
||||
- Aggro-Regeln (Werte in AggroConfig Resource):
|
||||
- Aufbau:
|
||||
@@ -254,6 +268,9 @@ autoloads/ — Globaler Zustand
|
||||
- HitArea (Area3D)
|
||||
- DetectionArea (Area3D, Auto-Targeting bei Betreten)
|
||||
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
|
||||
- HealthbarShield (Node, healthbar_shield.gd)
|
||||
- HealthbarStatus (Node, healthbar_status.gd)
|
||||
- HealthbarEffects (Node, healthbar_effects.gd)
|
||||
- portal.gd — Registriert bei Stats mit PortalStats Resource
|
||||
- Spawnt Gegner bei HP-Schwellen (→ SpawnSystem)
|
||||
|
||||
@@ -266,7 +283,7 @@ autoloads/ — Globaler Zustand
|
||||
|
||||
## Dungeon (dungeon/)
|
||||
- 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
|
||||
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
|
||||
- Spieler (Instanz von player.tscn)
|
||||
@@ -278,13 +295,15 @@ autoloads/ — Globaler Zustand
|
||||
- Eigene Systems bis Welt parallel läuft (geplant: Reparenting)
|
||||
|
||||
## HUD (hud/)
|
||||
- hud.tscn — CanvasLayer
|
||||
- hud.tscn — CanvasLayer (kein Root-Skript)
|
||||
- HealthBar (ProgressBar, Label)
|
||||
- ShieldBar (ProgressBar, Label)
|
||||
- EffectContainer (HBoxContainer, programmatisch, unter ShieldBar)
|
||||
- RespawnTimer (Label, Countdown bei Tod)
|
||||
- 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)
|
||||
- Schadens-Klasse:
|
||||
@@ -308,3 +327,6 @@ autoloads/ — Globaler Zustand
|
||||
- 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
|
||||
- 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD)
|
||||
|
||||
---
|
||||
|
||||
|
||||
72
plan2.md
Normal file
72
plan2.md
Normal 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 |
|
||||
@@ -18,8 +18,10 @@ config/icon="res://icon.svg"
|
||||
[autoload]
|
||||
|
||||
EventBus="*res://autoloads/event_bus.gd"
|
||||
Stats="*res://autoloads/stats/stats.gd"
|
||||
GameState="*res://autoloads/game_state.gd"
|
||||
PlayerData="*res://autoloads/player_stats.gd"
|
||||
EnemyData="*res://autoloads/enemy_stats.gd"
|
||||
BossData="*res://autoloads/boss_stats.gd"
|
||||
PortalData="*res://autoloads/portal_stats.gd"
|
||||
|
||||
[dotnet]
|
||||
|
||||
|
||||
@@ -3,23 +3,34 @@
|
||||
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"]
|
||||
[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"]
|
||||
[ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"]
|
||||
[ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
|
||||
[ext_resource type="Script" path="res://systems/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/health_system.gd" id="health_system"]
|
||||
[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
|
||||
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
|
||||
[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
|
||||
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
|
||||
[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
|
||||
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
||||
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
|
||||
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
|
||||
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
|
||||
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
|
||||
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
|
||||
[ext_resource type="Script" path="res://systems/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/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/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"]
|
||||
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"]
|
||||
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"]
|
||||
script = ExtResource("shield_system")
|
||||
|
||||
[node name="DamageSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("damage_system")
|
||||
[node name="RoleSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("role_system")
|
||||
tank_set = ExtResource("tank_set")
|
||||
damage_set = ExtResource("damage_set")
|
||||
healer_set = ExtResource("healer_set")
|
||||
|
||||
[node name="AbilitySystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("ability_system")
|
||||
|
||||
[node name="AttackSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("attack_system")
|
||||
|
||||
[node name="CooldownSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("cooldown_system")
|
||||
|
||||
[node name="TargetingSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("targeting_system")
|
||||
|
||||
[node name="AggroSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("aggro_system")
|
||||
|
||||
@@ -82,8 +108,8 @@ script = ExtResource("aggro_decay")
|
||||
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
|
||||
script = ExtResource("aggro_events")
|
||||
|
||||
[node name="EnemyAISystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("enemy_ai_system")
|
||||
[node name="AISystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("ai_system")
|
||||
|
||||
[node name="RespawnSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("respawn_system")
|
||||
@@ -91,12 +117,24 @@ script = ExtResource("respawn_system")
|
||||
[node name="SpawnSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("spawn_system")
|
||||
|
||||
[node name="EffectSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("effect_system")
|
||||
[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")
|
||||
|
||||
[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="."]
|
||||
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")]
|
||||
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)
|
||||
stats = ExtResource("boss_stats")
|
||||
|
||||
[node name="ExitGate" parent="." instance=ExtResource("gate")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
|
||||
target_scene = "res://scenes/world/world.tscn"
|
||||
is_exit = true
|
||||
|
||||
[node name="DungeonManager" type="Node" parent="."]
|
||||
script = ExtResource("dungeon_manager")
|
||||
[node name="DungeonSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("dungeon_system")
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://drn4h1lxx5t1j
|
||||
@@ -1,5 +0,0 @@
|
||||
extends "res://scenes/enemy/enemy.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
add_to_group("boss")
|
||||
@@ -1 +0,0 @@
|
||||
uid://bkehq3dqyp2yd
|
||||
@@ -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"]
|
||||
@@ -1 +1 @@
|
||||
uid://bio01w2gd5e7q
|
||||
uid://dlawq281oesnf
|
||||
|
||||
11
scenes/enemy/detection.gd
Normal file
11
scenes/enemy/detection.gd
Normal 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)
|
||||
1
scenes/enemy/detection.gd.uid
Normal file
1
scenes/enemy/detection.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b07aajhufqvb3
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bwi75jx0agktd
|
||||
@@ -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" 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/enemy_stats.tres" id="8"]
|
||||
[ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"]
|
||||
[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||
radius = 0.4
|
||||
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"]
|
||||
radius = 0.4
|
||||
height = 1.5
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
||||
radius = 10.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
|
||||
bg_color = Color(0.3, 0.1, 0.1, 1)
|
||||
|
||||
@@ -25,74 +33,58 @@ 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.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"]
|
||||
[node name="Enemy" type="CharacterBody3D" unique_id=1724620529]
|
||||
script = ExtResource("1")
|
||||
stats = ExtResource("8")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038]
|
||||
shape = SubResource("CapsuleShape3D_1")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1598094615]
|
||||
mesh = SubResource("SphereMesh_1")
|
||||
|
||||
[node name="HitArea" type="Area3D" parent="."]
|
||||
[node name="HitArea" type="Area3D" parent="." unique_id=893463784]
|
||||
collision_layer = 4
|
||||
collision_mask = 0
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962]
|
||||
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="."]
|
||||
script = ExtResource("5")
|
||||
[node name="Detection" type="Node" parent="." unique_id=534240144]
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="DetectionArea" type="Area3D" parent="."]
|
||||
[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598]
|
||||
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")
|
||||
|
||||
[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)
|
||||
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
|
||||
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_bottom = 29.0
|
||||
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_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 = 100.0
|
||||
value = 100.0
|
||||
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_top = 15.0
|
||||
offset_right = 102.0
|
||||
@@ -103,5 +95,5 @@ max_value = 50.0
|
||||
value = 50.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"]
|
||||
[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"]
|
||||
[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"]
|
||||
|
||||
@@ -1,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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://tnx6rbnnngn
|
||||
30
scenes/enemy/init.gd
Normal file
30
scenes/enemy/init.gd
Normal 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
1
scenes/enemy/init.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vy6hyqok0p8b
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://d1w7vm7t3k3ts
|
||||
@@ -1 +0,0 @@
|
||||
uid://c4jhr8k4uwoy7
|
||||
@@ -1,7 +1,5 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"]
|
||||
bg_color = Color(0.2, 0.2, 0.2, 0.8)
|
||||
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"]
|
||||
bg_color = Color(0.2, 0.5, 0.9, 1)
|
||||
|
||||
[node name="HUD" type="CanvasLayer"]
|
||||
script = ExtResource("1")
|
||||
[node name="HUD" type="CanvasLayer" groups=["hud"]]
|
||||
|
||||
[node name="HealthBar" type="ProgressBar" parent="."]
|
||||
offset_left = 10.0
|
||||
|
||||
@@ -5,5 +5,5 @@ extends Node
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
for i in range(5):
|
||||
if event.is_action_pressed("ability_%s" % (i + 1)):
|
||||
EventBus.ability_use_requested.emit(player, i)
|
||||
EventBus.ability_use.emit(player, i)
|
||||
return
|
||||
1
scenes/player/ability.gd.uid
Normal file
1
scenes/player/ability.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://hh5yw7vcjdqr
|
||||
@@ -1 +0,0 @@
|
||||
uid://d15til6fsxw5b
|
||||
12
scenes/player/init.gd
Normal file
12
scenes/player/init.gd
Normal 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()
|
||||
1
scenes/player/init.gd.uid
Normal file
1
scenes/player/init.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cx6k5473yxno
|
||||
@@ -8,12 +8,8 @@ func _physics_process(delta: float) -> void:
|
||||
if not player.is_on_floor():
|
||||
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():
|
||||
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 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()
|
||||
|
||||
if direction:
|
||||
player.velocity.x = direction.x * speed
|
||||
player.velocity.z = direction.z * speed
|
||||
player.velocity.x = direction.x * PlayerData.speed
|
||||
player.velocity.z = direction.z * PlayerData.speed
|
||||
else:
|
||||
player.velocity.x = move_toward(player.velocity.x, 0, speed)
|
||||
player.velocity.z = move_toward(player.velocity.z, 0, speed)
|
||||
player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed)
|
||||
player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed)
|
||||
|
||||
player.move_and_slide()
|
||||
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bfpt2p7uucfyb
|
||||
@@ -1,15 +1,12 @@
|
||||
[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://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" 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://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"]
|
||||
[ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"]
|
||||
[ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||
radius = 0.3
|
||||
@@ -19,34 +16,31 @@ height = 1.8
|
||||
radius = 0.3
|
||||
height = 1.8
|
||||
|
||||
[node name="Player" type="CharacterBody3D" unique_id=1350215040]
|
||||
[node name="Player" type="CharacterBody3D" unique_id=197716516]
|
||||
script = ExtResource("1")
|
||||
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")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1346931672]
|
||||
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1514179122]
|
||||
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)
|
||||
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)
|
||||
|
||||
[node name="Movement" type="Node" parent="." unique_id=654979387]
|
||||
[node name="Movement" type="Node" parent="." unique_id=811179177]
|
||||
script = ExtResource("3")
|
||||
|
||||
[node name="Combat" type="Node" parent="." unique_id=1754235583]
|
||||
[node name="Ability" type="Node" parent="." unique_id=1184596245]
|
||||
script = ExtResource("4")
|
||||
|
||||
[node name="Targeting" type="Node" parent="." unique_id=592540710]
|
||||
[node name="Targeting" type="Node" parent="." unique_id=1974574662]
|
||||
script = ExtResource("8")
|
||||
|
||||
[node name="Role" type="Node" parent="." unique_id=134158295]
|
||||
[node name="Role" type="Node" parent="." unique_id=1637643687]
|
||||
script = ExtResource("10")
|
||||
tank_set = ExtResource("11")
|
||||
damage_set = ExtResource("12")
|
||||
healer_set = ExtResource("13")
|
||||
|
||||
@@ -1,40 +1,14 @@
|
||||
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()
|
||||
|
||||
func _ready() -> void:
|
||||
set_role.call_deferred(current_role)
|
||||
EventBus.role_change_requested.emit(player, PlayerData.current_role)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
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"):
|
||||
set_role(Role.DAMAGE)
|
||||
EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE)
|
||||
elif event.is_action_pressed("class_healer"):
|
||||
set_role(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
|
||||
EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER)
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
extends Node
|
||||
|
||||
const TARGET_RANGE := 20.0
|
||||
const COMBAT_TIMEOUT := 3.0
|
||||
|
||||
var current_target: Node3D = null
|
||||
var mouse_press_pos: Vector2 = Vector2.ZERO
|
||||
var in_combat := false
|
||||
var combat_timer := 0.0
|
||||
|
||||
@onready var player: CharacterBody3D = get_parent()
|
||||
@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:
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
@@ -44,71 +29,19 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void:
|
||||
var result := space.intersect_ray(query)
|
||||
if result:
|
||||
var hit_target := result.collider.get_parent() as Node3D
|
||||
set_target(hit_target)
|
||||
EventBus.target_requested.emit(player, hit_target)
|
||||
else:
|
||||
set_target(null)
|
||||
EventBus.target_requested.emit(player, null)
|
||||
|
||||
func _cycle_target() -> void:
|
||||
var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
|
||||
if targets.is_empty():
|
||||
set_target(null)
|
||||
EventBus.target_requested.emit(player, null)
|
||||
return
|
||||
if current_target == null or current_target not in targets:
|
||||
set_target(targets[0])
|
||||
var current: Node3D = PlayerData.target
|
||||
if current == null or current not in targets:
|
||||
EventBus.target_requested.emit(player, targets[0])
|
||||
return
|
||||
var idx := targets.find(current_target)
|
||||
var idx := targets.find(current)
|
||||
var next_idx := (idx + 1) % targets.size()
|
||||
set_target(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)
|
||||
EventBus.target_requested.emit(player, targets[next_idx])
|
||||
|
||||
@@ -7,7 +7,7 @@ var active := false
|
||||
|
||||
func _ready() -> void:
|
||||
if not is_exit:
|
||||
if GameState.dungeon_cleared:
|
||||
if PlayerData.dungeon_cleared:
|
||||
queue_free()
|
||||
return
|
||||
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:
|
||||
return
|
||||
if body is CharacterBody3D and body.name == "Player":
|
||||
GameState.save_player(body)
|
||||
PlayerData.save_cache()
|
||||
if is_exit:
|
||||
GameState.returning_from_dungeon = true
|
||||
PlayerData.returning_from_dungeon = true
|
||||
else:
|
||||
GameState.portal_position = global_position
|
||||
PlayerData.portal_position = global_position
|
||||
call_deferred("_change_scene")
|
||||
|
||||
func _change_scene() -> void:
|
||||
|
||||
14
scenes/portal/init.gd
Normal file
14
scenes/portal/init.gd
Normal 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)
|
||||
1
scenes/portal/init.gd.uid
Normal file
1
scenes/portal/init.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dlexijybbqxop
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://byjxj4mq84gki
|
||||
@@ -1,7 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/portal/portal.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://scenes/portal/init.gd" id="1"]
|
||||
[ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"]
|
||||
|
||||
[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)
|
||||
billboard = 1
|
||||
pixel_size = 0.01
|
||||
script = ExtResource("3")
|
||||
|
||||
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
|
||||
transparent_bg = true
|
||||
|
||||
@@ -11,17 +11,17 @@ var portals: Array[Node] = []
|
||||
var timer := 0.0
|
||||
|
||||
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")
|
||||
else:
|
||||
if GameState.dungeon_cleared:
|
||||
GameState.clear()
|
||||
if PlayerData.dungeon_cleared:
|
||||
PlayerData.clear_cache()
|
||||
call_deferred("_spawn_portal")
|
||||
|
||||
func _restore_gate() -> void:
|
||||
var gate: Node3D = GATE_SCENE.instantiate()
|
||||
get_parent().add_child(gate)
|
||||
gate.global_position = GameState.portal_position
|
||||
gate.global_position = PlayerData.portal_position
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
timer += delta
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
[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" uid="uid://cysg30lud2ta2" 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" uid="uid://cm7ehl2pexcst" 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" uid="uid://ddos7mo8rahou" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
||||
[ext_resource type="Script" uid="uid://cbd1bryh0e2dw" path="res://systems/damage_system.gd" id="damage_system"]
|
||||
[ext_resource type="Script" uid="uid://drdlh6tq0dfwo" path="res://systems/effect_system.gd" id="effect_system"]
|
||||
[ext_resource type="Script" uid="uid://bqebxfvticxto" path="res://systems/element_system.gd" id="element_system"]
|
||||
[ext_resource type="Script" uid="uid://bwhxu5586lc1l" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"]
|
||||
[ext_resource type="Script" uid="uid://b3wkn5118dimy" path="res://systems/health_system.gd" id="health_system"]
|
||||
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
|
||||
[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_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/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/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/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" 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" uid="uid://b1qkvoqvmd21h" path="res://systems/respawn_system.gd" id="respawn_system"]
|
||||
[ext_resource type="Script" uid="uid://rsnpuf77o0sn" path="res://systems/shield_system.gd" id="shield_system"]
|
||||
[ext_resource type="Script" uid="uid://c84voxmnaifyt" path="res://systems/spawn_system.gd" id="spawn_system"]
|
||||
[ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"]
|
||||
[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"]
|
||||
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"]
|
||||
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")
|
||||
|
||||
[node name="ShieldSystem" type="Node" parent="Systems" unique_id=1790230220]
|
||||
script = ExtResource("shield_system")
|
||||
|
||||
[node name="DamageSystem" type="Node" parent="Systems" unique_id=2146323526]
|
||||
[node name="DamageSystem" type="Node" parent="Systems"]
|
||||
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")
|
||||
|
||||
[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")
|
||||
|
||||
[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")
|
||||
|
||||
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem" unique_id=1597893665]
|
||||
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"]
|
||||
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")
|
||||
|
||||
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem" unique_id=1936723580]
|
||||
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
|
||||
script = ExtResource("aggro_events")
|
||||
|
||||
[node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042]
|
||||
script = ExtResource("enemy_ai_system")
|
||||
[node name="AISystem" type="Node" parent="Systems"]
|
||||
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")
|
||||
|
||||
[node name="SpawnSystem" type="Node" parent="Systems" unique_id=1099032666]
|
||||
[node name="SpawnSystem" type="Node" parent="Systems"]
|
||||
script = ExtResource("spawn_system")
|
||||
|
||||
[node name="EffectSystem" type="Node" parent="Systems" unique_id=1219368182]
|
||||
script = ExtResource("effect_system")
|
||||
[node name="PortalSystem" type="Node" parent="Systems"]
|
||||
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")
|
||||
|
||||
[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)
|
||||
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")
|
||||
|
||||
[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")
|
||||
|
||||
[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)
|
||||
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)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Taverne" unique_id=2043279810]
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Taverne"]
|
||||
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")
|
||||
|
||||
[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)
|
||||
|
||||
[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")
|
||||
|
||||
@@ -1,124 +1,83 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.ability_use_requested.connect(_on_ability_use_requested)
|
||||
EventBus.ability_use.connect(_on_ability_use)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var players := get_tree().get_nodes_in_group("player")
|
||||
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:
|
||||
func _on_ability_use(_player: Node, ability_index: int) -> void:
|
||||
if not PlayerData.alive:
|
||||
return
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
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()
|
||||
var ability_set: AbilitySet = PlayerData.ability_set
|
||||
if not ability_set or ability_index >= ability_set.abilities.size():
|
||||
return
|
||||
var ability: Ability = ability_set.abilities[ability_index]
|
||||
if not ability or ability.type == Ability.Type.PASSIVE:
|
||||
return
|
||||
var cooldown_system: Node = get_node("../CooldownSystem")
|
||||
if not cooldown_system.is_ready(player, ability_index):
|
||||
if PlayerData.cooldowns[ability_index] > 0:
|
||||
return
|
||||
if ability.uses_gcd and not cooldown_system.is_gcd_ready(player):
|
||||
if ability.uses_gcd and PlayerData.gcd > 0:
|
||||
return
|
||||
var success: bool = _execute_ability(player, ability)
|
||||
var success: bool = _execute_ability(ability)
|
||||
if not success:
|
||||
return
|
||||
var player_base: BaseStats = Stats.get_base(player)
|
||||
var gcd_time: float = player_base.gcd_time if player_base is PlayerStats else 0.5
|
||||
var gcd: float = gcd_time if ability.uses_gcd else 0.0
|
||||
cooldown_system.set_cooldown(player, ability_index, ability.cooldown, gcd)
|
||||
var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0
|
||||
PlayerData.cooldowns[ability_index] = ability.cooldown
|
||||
PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd)
|
||||
if gcd > 0:
|
||||
PlayerData.gcd = gcd
|
||||
|
||||
func _execute_ability(player: Node, ability: Ability) -> bool:
|
||||
var targeting: Node = player.get_node("Targeting")
|
||||
func _execute_ability(ability: Ability) -> bool:
|
||||
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:
|
||||
Ability.Type.SINGLE:
|
||||
return _execute_single(player, targeting, ability, dmg)
|
||||
return _execute_single(player, ability, dmg)
|
||||
Ability.Type.AOE:
|
||||
return _execute_aoe(player, ability, dmg)
|
||||
Ability.Type.UTILITY:
|
||||
return _execute_utility(player, ability)
|
||||
return _execute_utility(ability)
|
||||
Ability.Type.ULT:
|
||||
return _execute_ult(player, targeting, ability, dmg)
|
||||
return _execute_ult(player, ability, dmg)
|
||||
return false
|
||||
|
||||
func _apply_passive(player: Node, base: float, stat: String) -> float:
|
||||
var mult: Variant = Stats.get_stat(player, "buff_" + stat)
|
||||
if mult != null:
|
||||
return base * mult
|
||||
return base
|
||||
func _apply_passive(base: float, stat: String) -> float:
|
||||
var mult: float = 1.0
|
||||
match stat:
|
||||
"damage": mult = PlayerData.buff_damage
|
||||
"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:
|
||||
return true
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
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
|
||||
|
||||
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:
|
||||
EventBus.heal_requested.emit(player, player, dmg)
|
||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||
return true
|
||||
if not _in_range(player, targeting, ability):
|
||||
if not _in_range(ability):
|
||||
return false
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
|
||||
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
|
||||
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)
|
||||
return true
|
||||
|
||||
func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
|
||||
if ability.is_heal:
|
||||
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)
|
||||
return true
|
||||
var hit := false
|
||||
var enemies := get_tree().get_nodes_in_group("enemies")
|
||||
for enemy in enemies:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
var dist: float = player.global_position.distance_to(enemy.global_position)
|
||||
if dist <= ability.ability_range:
|
||||
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)
|
||||
return hit
|
||||
|
||||
func _execute_utility(player: Node, ability: Ability) -> bool:
|
||||
var max_shield: float = Stats.get_stat(player, "max_shield")
|
||||
if max_shield <= 0:
|
||||
func _execute_utility(ability: Ability) -> bool:
|
||||
if PlayerData.max_shield <= 0:
|
||||
return false
|
||||
var shield: float = Stats.get_stat(player, "shield")
|
||||
var shield: float = PlayerData.shield
|
||||
if ability.damage > 0:
|
||||
shield = max_shield * (ability.damage / 100.0)
|
||||
shield = PlayerData.max_shield * (ability.damage / 100.0)
|
||||
else:
|
||||
if shield >= max_shield:
|
||||
if shield >= PlayerData.max_shield:
|
||||
return false
|
||||
shield = max_shield
|
||||
Stats.set_stat(player, "shield", shield)
|
||||
EventBus.shield_changed.emit(player, shield, max_shield)
|
||||
shield = PlayerData.max_shield
|
||||
PlayerData.set_shield(shield)
|
||||
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:
|
||||
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
|
||||
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)
|
||||
return true
|
||||
if not _in_range(player, targeting, ability):
|
||||
if not _in_range(ability):
|
||||
return false
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
var target: Node3D = targeting.current_target
|
||||
var target: Node3D = PlayerData.target
|
||||
EventBus.damage_requested.emit(player, target, dmg * 5.0)
|
||||
if ability.element != 0:
|
||||
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 enemies := get_tree().get_nodes_in_group("enemies")
|
||||
for enemy in enemies:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if enemy != target and is_instance_valid(enemy):
|
||||
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
|
||||
if enemy_dist <= splash_range:
|
||||
|
||||
@@ -23,8 +23,12 @@ func _update_combat_timers(delta: float) -> void:
|
||||
|
||||
func _decay_aggro(enemy: Node, delta: float) -> void:
|
||||
var table: Dictionary = tracker.aggro_tables[enemy]
|
||||
var base: BaseStats = Stats.get_base(enemy)
|
||||
var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0
|
||||
var data_source: Node = tracker._get_data_source(enemy)
|
||||
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():
|
||||
if is_in_combat(player):
|
||||
continue
|
||||
@@ -58,10 +62,11 @@ func spread_aggro(source: Node, attacker: Node, amount: float) -> void:
|
||||
func alert_nearby(enemy: Node, target: Node) -> void:
|
||||
var radius: float = tracker.get_alert_radius(enemy)
|
||||
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)
|
||||
other.target = target
|
||||
other.state = other.State.CHASE
|
||||
data_source.set_stat(other, "target", target)
|
||||
data_source.set_stat(other, "state", 1)
|
||||
EventBus.enemy_engaged.emit(other, target)
|
||||
|
||||
func erase_entity(entity: Node) -> void:
|
||||
|
||||
@@ -14,14 +14,16 @@ func _ready() -> void:
|
||||
func _on_enemy_detected(enemy: Node, player: Node) -> void:
|
||||
if not enemy.is_in_group("enemies"):
|
||||
return
|
||||
if "state" in enemy:
|
||||
if enemy.state == enemy.State.CHASE or enemy.state == enemy.State.ATTACK:
|
||||
var data_source: Node = tracker._get_data_source(enemy)
|
||||
if data_source:
|
||||
var state: int = data_source.get_stat(enemy, "state")
|
||||
if state == 1 or state == 2:
|
||||
return
|
||||
tracker.add_player_in_range(enemy, player)
|
||||
tracker.add_aggro(enemy, player, 1.0)
|
||||
if "state" in enemy:
|
||||
enemy.target = player
|
||||
enemy.state = enemy.State.CHASE
|
||||
if data_source:
|
||||
data_source.set_stat(enemy, "target", player)
|
||||
data_source.set_stat(enemy, "state", 1)
|
||||
EventBus.enemy_engaged.emit(enemy, player)
|
||||
decay.alert_nearby(enemy, player)
|
||||
|
||||
@@ -33,8 +35,7 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
|
||||
return
|
||||
decay.reset_combat_timer(attacker)
|
||||
var multiplier := 1.0
|
||||
var role: Node = attacker.get_node_or_null("Role")
|
||||
if role and role.current_role == 0:
|
||||
if PlayerData.current_role == PlayerData.Role.TANK:
|
||||
multiplier = config.tank_multiplier
|
||||
var aggro: float = amount * multiplier
|
||||
tracker.add_aggro(target, attacker, aggro)
|
||||
|
||||
@@ -43,17 +43,19 @@ func get_top_target(table: Dictionary) -> Node:
|
||||
return top
|
||||
|
||||
func update_target(enemy: Node) -> void:
|
||||
if not "state" in enemy:
|
||||
return
|
||||
var table: Dictionary = aggro_tables[enemy]
|
||||
var top: Node = get_top_target(table)
|
||||
if top and top != enemy.target:
|
||||
enemy.target = top
|
||||
if enemy.state == enemy.State.IDLE or enemy.state == enemy.State.RETURN:
|
||||
enemy.state = enemy.State.CHASE
|
||||
elif not top and enemy.state != enemy.State.IDLE and enemy.state != enemy.State.RETURN:
|
||||
enemy.target = null
|
||||
enemy.state = enemy.State.RETURN
|
||||
var data_source: Node = _get_data_source(enemy)
|
||||
if not data_source:
|
||||
return
|
||||
var state: int = data_source.get_stat(enemy, "state")
|
||||
if top and top != data_source.get_stat(enemy, "target"):
|
||||
data_source.set_stat(enemy, "target", top)
|
||||
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:
|
||||
var result: Array = []
|
||||
@@ -65,8 +67,12 @@ func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
||||
return result
|
||||
|
||||
func get_alert_radius(entity: Node) -> float:
|
||||
var base: BaseStats = Stats.get_base(entity)
|
||||
return base.alert_radius if base is EnemyStats else 10.0
|
||||
var data_source: Node = _get_data_source(entity)
|
||||
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:
|
||||
aggro_tables.erase(entity)
|
||||
@@ -74,9 +80,17 @@ func erase_entity(entity: Node) -> void:
|
||||
for enemy in aggro_tables:
|
||||
if is_instance_valid(enemy):
|
||||
aggro_tables[enemy].erase(entity)
|
||||
if "target" in enemy and entity == enemy.target:
|
||||
enemy.target = null
|
||||
enemy.state = enemy.State.RETURN
|
||||
var data_source: Node = _get_data_source(enemy)
|
||||
if data_source and data_source.get_stat(enemy, "target") == entity:
|
||||
data_source.set_stat(enemy, "target", null)
|
||||
data_source.set_stat(enemy, "state", 3)
|
||||
for enemy in players_in_range:
|
||||
if is_instance_valid(enemy):
|
||||
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
91
systems/ai_system.gd
Normal 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
1
systems/ai_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dokr1ut7ea541
|
||||
27
systems/attack_system.gd
Normal file
27
systems/attack_system.gd
Normal 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
|
||||
1
systems/attack_system.gd.uid
Normal file
1
systems/attack_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvuds0uuffj6t
|
||||
62
systems/aura_system.gd
Normal file
62
systems/aura_system.gd
Normal 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)
|
||||
1
systems/aura_system.gd.uid
Normal file
1
systems/aura_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b17o3hfdm8uo6
|
||||
142
systems/buff_system.gd
Normal file
142
systems/buff_system.gd
Normal 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"])
|
||||
1
systems/buff_system.gd.uid
Normal file
1
systems/buff_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://y2bm5ssu77wp
|
||||
@@ -1,73 +1,19 @@
|
||||
extends Node
|
||||
|
||||
var cooldowns: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("cooldown_system")
|
||||
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:
|
||||
for entity in cooldowns:
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var data: Dictionary = cooldowns[entity]
|
||||
if data["gcd"] > 0:
|
||||
data["gcd"] -= delta
|
||||
if data["aa"] > 0:
|
||||
data["aa"] -= delta
|
||||
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"])
|
||||
if PlayerData.gcd > 0:
|
||||
PlayerData.gcd -= delta
|
||||
if PlayerData.aa_timer > 0:
|
||||
PlayerData.aa_timer -= delta
|
||||
for i in range(PlayerData.cooldowns.size()):
|
||||
if PlayerData.cooldowns[i] > 0:
|
||||
PlayerData.cooldowns[i] -= delta
|
||||
EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.gcd)
|
||||
|
||||
func is_ready(entity: Node, index: int) -> bool:
|
||||
if entity not in cooldowns:
|
||||
return false
|
||||
return cooldowns[entity]["cds"][index] <= 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
|
||||
func _on_role_changed(_player: Node, _role_type: int) -> void:
|
||||
PlayerData.cooldowns.fill(0.0)
|
||||
PlayerData.max_cooldowns.fill(0.0)
|
||||
PlayerData.gcd = 0.0
|
||||
|
||||
@@ -1 +1,38 @@
|
||||
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")
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://cbd1bryh0e2dw
|
||||
uid://cmy1kqo1pk1q8
|
||||
|
||||
65
systems/debuff_system.gd
Normal file
65
systems/debuff_system.gd
Normal 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)
|
||||
1
systems/debuff_system.gd.uid
Normal file
1
systems/debuff_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ce12ledregjqx
|
||||
@@ -1,16 +1,13 @@
|
||||
extends Node
|
||||
|
||||
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)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity.is_in_group("boss"):
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
GameState.dungeon_cleared = true
|
||||
GameState.returning_from_dungeon = false
|
||||
GameState.clear()
|
||||
PlayerData.dungeon_cleared = true
|
||||
PlayerData.returning_from_dungeon = false
|
||||
PlayerData.clear_cache()
|
||||
EventBus.dungeon_cleared.emit()
|
||||
get_tree().change_scene_to_file("res://scenes/world/world.tscn")
|
||||
1
systems/dungeon_system.gd.uid
Normal file
1
systems/dungeon_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://lc5n3uxi4fho
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://drdlh6tq0dfwo
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://bwhxu5586lc1l
|
||||
22
systems/heal_system.gd
Normal file
22
systems/heal_system.gd
Normal 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")
|
||||
1
systems/heal_system.gd.uid
Normal file
1
systems/heal_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8jyik37e4tjw
|
||||
@@ -1,14 +1,25 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.damage_requested.connect(_on_damage_requested)
|
||||
EventBus.heal_requested.connect(_on_heal_requested)
|
||||
_emit_initial.call_deferred()
|
||||
|
||||
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):
|
||||
continue
|
||||
var data: Dictionary = Stats.entities[entity]
|
||||
var data: Dictionary = entities[entity]
|
||||
if not data["alive"]:
|
||||
continue
|
||||
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"])
|
||||
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
|
||||
|
||||
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
|
||||
if not Stats.is_alive(target):
|
||||
return
|
||||
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)
|
||||
func _emit_initial() -> void:
|
||||
EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health)
|
||||
EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://b3wkn5118dimy
|
||||
uid://h362ftxb0cns
|
||||
|
||||
@@ -1,75 +1,105 @@
|
||||
extends CanvasLayer
|
||||
extends Node
|
||||
|
||||
const GCD_TIME := 0.5
|
||||
|
||||
@onready var health_bar: ProgressBar = $HealthBar
|
||||
@onready var health_label: Label = $HealthBar/HealthLabel
|
||||
@onready var shield_bar: ProgressBar = $ShieldBar
|
||||
@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,
|
||||
]
|
||||
const ICON_SIZE := 20
|
||||
const FONT_SIZE := 14
|
||||
const BORDER_WIDTH := 2
|
||||
const MARGIN := 2
|
||||
|
||||
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
|
||||
var effect_container: HBoxContainer = null
|
||||
|
||||
func _ready() -> void:
|
||||
respawn_label.visible = false
|
||||
_create_effect_container()
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.shield_changed.connect(_on_shield_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.player_respawned.connect(_on_player_respawned)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.respawn_tick.connect(_on_respawn_tick)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.cooldown_tick.connect(_on_cooldown_tick)
|
||||
EventBus.effect_applied.connect(_on_effect_applied)
|
||||
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:
|
||||
if entity.name == "Player":
|
||||
health_bar.max_value = max_val
|
||||
health_bar.value = current
|
||||
health_label.text = "%d/%d" % [current, max_val]
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
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:
|
||||
if entity.name == "Player":
|
||||
shield_bar.max_value = max_val
|
||||
shield_bar.value = current
|
||||
shield_label.text = "%d/%d" % [current, max_val]
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
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:
|
||||
if entity.name == "Player":
|
||||
respawn_label.visible = true
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if hud:
|
||||
hud.get_node("RespawnTimer").visible = true
|
||||
|
||||
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:
|
||||
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:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label")
|
||||
match role_type:
|
||||
0: class_icon.text = "T"
|
||||
1: class_icon.text = "D"
|
||||
2: class_icon.text = "H"
|
||||
0: icon.text = "T"
|
||||
1: icon.text = "D"
|
||||
2: icon.text = "H"
|
||||
|
||||
func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void:
|
||||
for i in range(min(ability_panels.size(), cooldowns.size())):
|
||||
var panel: Panel = ability_panels[i]
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
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 overlay: ColorRect = panel.get_node("CooldownOverlay")
|
||||
var cd: float = cooldowns[i]
|
||||
var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0
|
||||
var active_cd: float = max(cd, gcd)
|
||||
var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME
|
||||
|
||||
if active_cd > 0:
|
||||
var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0)
|
||||
overlay.visible = true
|
||||
@@ -79,43 +109,19 @@ func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float)
|
||||
overlay.visible = false
|
||||
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:
|
||||
if target.name != "Player":
|
||||
if target != PlayerData:
|
||||
return
|
||||
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)
|
||||
if effect_container:
|
||||
_add_icon(effect)
|
||||
|
||||
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
||||
if target.name != "Player":
|
||||
if target != PlayerData:
|
||||
return
|
||||
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
|
||||
if effect_container:
|
||||
_remove_icon(effect)
|
||||
|
||||
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:
|
||||
func _add_icon(effect: Effect) -> void:
|
||||
var panel := PanelContainer.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
match effect.type:
|
||||
@@ -128,18 +134,35 @@ func _create_icon_panel(effect: Effect) -> PanelContainer:
|
||||
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(2)
|
||||
style.set_content_margin_all(2)
|
||||
style.set_border_width_all(BORDER_WIDTH)
|
||||
style.set_content_margin_all(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", 14)
|
||||
label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
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.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_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
|
||||
1
systems/hud_system.gd.uid
Normal file
1
systems/hud_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://da87wrxxuhws1
|
||||
225
systems/nameplate_system.gd
Normal file
225
systems/nameplate_system.gd
Normal 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
|
||||
1
systems/nameplate_system.gd.uid
Normal file
1
systems/nameplate_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yijhaxo8anul
|
||||
21
systems/portal_system.gd
Normal file
21
systems/portal_system.gd
Normal 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()
|
||||
1
systems/portal_system.gd.uid
Normal file
1
systems/portal_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5sqw08twxtnh
|
||||
@@ -1,48 +1,42 @@
|
||||
extends Node
|
||||
|
||||
var dead_players: Dictionary = {}
|
||||
var respawn_timer := 0.0
|
||||
var is_dead := false
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for player in dead_players.keys():
|
||||
if not is_instance_valid(player):
|
||||
dead_players.erase(player)
|
||||
continue
|
||||
dead_players[player] -= delta
|
||||
EventBus.respawn_tick.emit(dead_players[player])
|
||||
if dead_players[player] <= 0:
|
||||
_respawn(player)
|
||||
if not is_dead:
|
||||
return
|
||||
respawn_timer -= delta
|
||||
EventBus.respawn_tick.emit(respawn_timer)
|
||||
if respawn_timer <= 0:
|
||||
_respawn()
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if not entity.is_in_group("player"):
|
||||
return
|
||||
if entity in dead_players:
|
||||
if is_dead:
|
||||
return
|
||||
var base: BaseStats = Stats.get_base(entity)
|
||||
var respawn_time: float = base.respawn_time if base is PlayerStats else 3.0
|
||||
dead_players[entity] = respawn_time
|
||||
is_dead = true
|
||||
respawn_timer = PlayerData.respawn_time
|
||||
entity.velocity = Vector3.ZERO
|
||||
entity.get_node("Mesh").visible = false
|
||||
entity.get_node("CollisionShape3D").disabled = true
|
||||
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)
|
||||
|
||||
func _respawn(player: Node) -> void:
|
||||
dead_players.erase(player)
|
||||
func _respawn() -> void:
|
||||
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.get_node("Mesh").visible = true
|
||||
player.get_node("CollisionShape3D").disabled = false
|
||||
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)
|
||||
var max_health: float = Stats.get_stat(player, "max_health")
|
||||
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)
|
||||
PlayerData.respawn()
|
||||
|
||||
23
systems/role_system.gd
Normal file
23
systems/role_system.gd
Normal 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)
|
||||
1
systems/role_system.gd.uid
Normal file
1
systems/role_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cuwueo5v43kap
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user