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.
|
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
|
||||||
- `scenes/` — Darstellung + Input
|
- `scenes/` — Darstellung + Input
|
||||||
- `player/` — Spieler + player_stats + role/ (Rollen + Abilities)
|
- `player/` — Spieler + player_stats + role/ (Rollen + Abilities)
|
||||||
- `enemy/` — Gegner + Boss + enemy_stats + boss_stats
|
- `enemy/` — Gegner + enemy_stats + boss/ (Boss + boss_stats)
|
||||||
- `portal/` — Portal + Gate + portal_stats
|
- `portal/` — Portal + Gate + portal_stats
|
||||||
- `dungeon/` — Dungeon + dungeon_manager
|
- `dungeon/` — Dungeon + dungeon_manager
|
||||||
- `hud/` — HUD
|
- `hud/` — HUD
|
||||||
- `world/` — Hauptszene + portal_spawner
|
- `world/` — Hauptszene + portal_spawner
|
||||||
- `healthbar.gd` — Shared Component
|
- `effect_icon_factory.gd` — Shared Utility (Effekt-Icons)
|
||||||
|
- `healthbar*.gd` — Healthbar-Komponenten (health, shield, status, effects)
|
||||||
- `systems/` — Spiellogik
|
- `systems/` — Spiellogik
|
||||||
- 11 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element)
|
- 12 Systeme (health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, aura, buff_calc)
|
||||||
- `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten)
|
- `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten)
|
||||||
- `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config
|
- `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config
|
||||||
- `autoloads/` — Globaler Zustand
|
- `autoloads/` — Globaler Zustand
|
||||||
@@ -44,13 +45,6 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
|
|||||||
## Planungsdokument
|
## Planungsdokument
|
||||||
`plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand.
|
`plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand.
|
||||||
|
|
||||||
## Design-Dokumente
|
|
||||||
Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design-Docs:
|
|
||||||
- `story.md` — Gameplay-Loop, Szenarien, Steuerung
|
|
||||||
- `idden.md` — Alle Ideen, Kernphilosophie, Technik
|
|
||||||
- `Szenarien.md` — Ressourcenanfragen, Dungeons, Gemeinschaft, Endgame
|
|
||||||
- `Level 1.md` bis `Level 3.md` — Systeme nach Priorität
|
|
||||||
|
|
||||||
## Core Loop
|
## Core Loop
|
||||||
1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum)
|
1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum)
|
||||||
2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%)
|
2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%)
|
||||||
|
|||||||
64
autoloads/boss_stats.gd
Normal file
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
|
extends Node
|
||||||
|
|
||||||
# Intentionen (Input → System)
|
# Intentionen (Input → System)
|
||||||
signal ability_use_requested(player, ability_index)
|
signal ability_use(player, ability_index)
|
||||||
|
signal role_change_requested(player, role)
|
||||||
|
signal target_requested(player, target)
|
||||||
signal enemy_detected(enemy, player)
|
signal enemy_detected(enemy, player)
|
||||||
|
signal enemy_lost(enemy, player)
|
||||||
|
signal portal_entered(portal, player)
|
||||||
|
|
||||||
# Kampf
|
# Kampf
|
||||||
signal attack_executed(attacker, position, direction, damage)
|
signal attack_executed(attacker, position, direction, damage)
|
||||||
@@ -29,7 +33,6 @@ signal buff_changed(entity, stat, value)
|
|||||||
|
|
||||||
# Gegner
|
# Gegner
|
||||||
signal enemy_engaged(enemy, target)
|
signal enemy_engaged(enemy, target)
|
||||||
signal enemy_lost(enemy, player)
|
|
||||||
|
|
||||||
# Portal
|
# Portal
|
||||||
signal portal_spawn(portal, enemies)
|
signal portal_spawn(portal, enemies)
|
||||||
|
|||||||
@@ -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.
|
Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten.
|
||||||
```
|
```
|
||||||
scenes/ — Darstellung + Input
|
scenes/ — Darstellung + Input
|
||||||
healthbar.gd — Shared Component
|
effect_icon_factory.gd — Shared Utility (statisch, Effekt-Icon-Erstellung)
|
||||||
|
healthbar.gd — Health-Anzeige + Viewport-Setup
|
||||||
|
healthbar_shield.gd — Shield-Anzeige
|
||||||
|
healthbar_status.gd — Target-Border + Aggro-Farbwechsel
|
||||||
|
healthbar_effects.gd — Effekt-Icons auf Healthbar
|
||||||
player/ — Spieler + player_stats
|
player/ — Spieler + player_stats
|
||||||
role/ — Rollenwechsel + Ability/AbilitySet-Klassen
|
role/ — Rollenwechsel + Ability/AbilitySet-Klassen
|
||||||
damage/ — set.tres + abilities/
|
damage/ — set.tres + abilities/
|
||||||
tank/ — set.tres + abilities/
|
tank/ — set.tres + abilities/
|
||||||
healer/ — set.tres + abilities/
|
healer/ — set.tres + abilities/
|
||||||
enemy/ — Gegner + Boss + enemy_stats + boss_stats
|
enemy/ — Gegner + enemy_stats
|
||||||
|
boss/ — Boss + boss_stats
|
||||||
portal/ — Portal + Gate + portal_stats
|
portal/ — Portal + Gate + portal_stats
|
||||||
dungeon/ — Dungeon + dungeon_manager
|
dungeon/ — Dungeon + dungeon_manager
|
||||||
hud/ — HUD
|
hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects)
|
||||||
world/ — Hauptszene + portal_spawner
|
world/ — Hauptszene + portal_spawner
|
||||||
systems/ — Spiellogik
|
systems/ — Spiellogik
|
||||||
aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config
|
aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config
|
||||||
effect.gd — Effect Resource (Buff/Debuff/Aura Daten)
|
effect.gd — Effect Resource (Buff/Debuff/Aura Daten)
|
||||||
10× *_system.gd — health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element
|
11× *_system.gd — health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, buff_calc
|
||||||
|
aura_system.gd — Aura-Propagierung (Child von EffectSystem)
|
||||||
autoloads/ — Globaler Zustand
|
autoloads/ — Globaler Zustand
|
||||||
event_bus.gd
|
event_bus.gd
|
||||||
game_state.gd
|
game_state.gd
|
||||||
@@ -27,7 +33,7 @@ autoloads/ — Globaler Zustand
|
|||||||
|
|
||||||
## Szenenbaum
|
## Szenenbaum
|
||||||
- Welt
|
- Welt
|
||||||
- Systems (11 Systeme als Child-Nodes)
|
- Systems (12 Systeme als Child-Nodes)
|
||||||
- Taverne
|
- Taverne
|
||||||
- Player
|
- Player
|
||||||
- Portale (dynamisch)
|
- Portale (dynamisch)
|
||||||
@@ -136,20 +142,26 @@ autoloads/ — Globaler Zustand
|
|||||||
- Listener: entity_died
|
- Listener: entity_died
|
||||||
- Event: respawn_tick, player_respawned
|
- Event: respawn_tick, player_respawned
|
||||||
### AbilitySystem (ability_system.gd)
|
### AbilitySystem (ability_system.gd)
|
||||||
- Ability-Ausführung (Single, AOE, Utility, Ult) + Auto-Attack in _process
|
- Ability-Ausführung (Single, AOE, Utility, Ult)
|
||||||
- Listener: ability_use_requested
|
- Listener: ability_use_requested
|
||||||
- Event: attack_executed, damage_requested, heal_requested
|
- Event: attack_executed, damage_requested, heal_requested
|
||||||
|
### AutoAttackSystem (auto_attack_system.gd)
|
||||||
|
- Auto-Attack-Logik, läuft jeden Frame in _process
|
||||||
|
- Liest CombatState.in_combat + Targeting.current_target
|
||||||
|
- Event: damage_requested, heal_requested
|
||||||
### EffectSystem (effect_system.gd)
|
### EffectSystem (effect_system.gd)
|
||||||
- Verwaltet Buffs, Debuffs und Auras auf Entities
|
- Verwaltet Buffs, Debuffs und Auras auf Entities
|
||||||
- Effect Resource (effect.gd): effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration, is_multiplier, aura_radius, tick_interval, element
|
- Effect Resource (effect.gd): effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration, is_multiplier, aura_radius, tick_interval, element
|
||||||
- State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer)
|
- State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer)
|
||||||
- Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt
|
- Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt
|
||||||
- Passive-Abilities werden als AURA-Effekte erstellt (role_changed → permanente Auras)
|
- Passive-Abilities werden als AURA-Effekte erstellt (role_changed → permanente Auras)
|
||||||
- Aura-Propagierung: Auras mit aura_radius > 0 geben allen Spielern im Radius einen temporären Buff (0.5s, refreshed jedes Frame). Verlässt man den Radius → Buff sofort weg
|
- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks
|
||||||
- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks, Aura-Propagierung
|
|
||||||
- _recalc_stat_buffs: Multiplier-Effekte aggregieren → buff_damage/heal/shield in Stats
|
|
||||||
- Listener: role_changed, entity_died, effect_requested
|
- Listener: role_changed, entity_died, effect_requested
|
||||||
- Event: buff_changed, shield_changed, effect_applied, effect_expired
|
- Event: effect_applied, effect_expired
|
||||||
|
- Children:
|
||||||
|
- AuraSystem (aura_system.gd) — Aura-Propagierung im Radius, Buff-Refresh
|
||||||
|
- BuffCalcSystem (buff_calc_system.gd) — Multiplier aggregieren → Stats + Shield updaten
|
||||||
|
- Event: buff_changed, shield_changed
|
||||||
### ElementSystem (element_system.gd)
|
### ElementSystem (element_system.gd)
|
||||||
- Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus
|
- Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus
|
||||||
- Element Enum: NONE, FIRE (erweiterbar)
|
- Element Enum: NONE, FIRE (erweiterbar)
|
||||||
@@ -164,8 +176,6 @@ autoloads/ — Globaler Zustand
|
|||||||
- Cooldown-Tracking, GCD, AA-Timer per Entity
|
- Cooldown-Tracking, GCD, AA-Timer per Entity
|
||||||
- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem
|
- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem
|
||||||
- Event: cooldown_tick
|
- Event: cooldown_tick
|
||||||
### DamageSystem (damage_system.gd)
|
|
||||||
- Reserviert für spätere Schadensberechnung (aktuell leer)
|
|
||||||
### AggroSystem (systems/aggro/)
|
### AggroSystem (systems/aggro/)
|
||||||
- Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres)
|
- Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres)
|
||||||
- aggro_system.gd — Parent, Config halten, Children verdrahten
|
- aggro_system.gd — Parent, Config halten, Children verdrahten
|
||||||
@@ -209,8 +219,9 @@ autoloads/ — Globaler Zustand
|
|||||||
- Camera3D
|
- Camera3D
|
||||||
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
|
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
|
||||||
- Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested
|
- Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested
|
||||||
|
- CombatState (Node, combat_state.gd) — in_combat-Tracking, Combat-Timer
|
||||||
- Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready)
|
- Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready)
|
||||||
- Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested
|
- Targeting (Node, targeting.gd) — Klick/TAB/Auto-Target, emittiert target_changed
|
||||||
- player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn
|
- player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn
|
||||||
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung
|
- camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung
|
||||||
|
|
||||||
@@ -223,7 +234,10 @@ autoloads/ — Globaler Zustand
|
|||||||
- DetectionArea (Area3D, emittiert enemy_detected)
|
- DetectionArea (Area3D, emittiert enemy_detected)
|
||||||
- NavigationAgent3D
|
- NavigationAgent3D
|
||||||
- EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle
|
- EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle
|
||||||
- Healthbar (Sprite3D + SubViewport, healthbar.gd) — liest HP/Shield von Stats, zeigt Effekt-Icons
|
- Healthbar (Sprite3D + SubViewport, healthbar.gd) — Health-Anzeige
|
||||||
|
- HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige
|
||||||
|
- HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe
|
||||||
|
- HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons
|
||||||
- enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal
|
- enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal
|
||||||
- Aggro-Regeln (Werte in AggroConfig Resource):
|
- Aggro-Regeln (Werte in AggroConfig Resource):
|
||||||
- Aufbau:
|
- Aufbau:
|
||||||
@@ -254,6 +268,9 @@ autoloads/ — Globaler Zustand
|
|||||||
- HitArea (Area3D)
|
- HitArea (Area3D)
|
||||||
- DetectionArea (Area3D, Auto-Targeting bei Betreten)
|
- DetectionArea (Area3D, Auto-Targeting bei Betreten)
|
||||||
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
|
- Healthbar (Sprite3D + SubViewport, healthbar.gd)
|
||||||
|
- HealthbarShield (Node, healthbar_shield.gd)
|
||||||
|
- HealthbarStatus (Node, healthbar_status.gd)
|
||||||
|
- HealthbarEffects (Node, healthbar_effects.gd)
|
||||||
- portal.gd — Registriert bei Stats mit PortalStats Resource
|
- portal.gd — Registriert bei Stats mit PortalStats Resource
|
||||||
- Spawnt Gegner bei HP-Schwellen (→ SpawnSystem)
|
- Spawnt Gegner bei HP-Schwellen (→ SpawnSystem)
|
||||||
|
|
||||||
@@ -266,7 +283,7 @@ autoloads/ — Globaler Zustand
|
|||||||
|
|
||||||
## Dungeon (dungeon/)
|
## Dungeon (dungeon/)
|
||||||
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
|
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
|
||||||
- Systems (alle 11 Systeme, temporär bis Welt parallel läuft)
|
- Systems (alle 12 Systeme, temporär bis Welt parallel läuft)
|
||||||
- NavigationRegion3D
|
- NavigationRegion3D
|
||||||
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
|
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
|
||||||
- Spieler (Instanz von player.tscn)
|
- Spieler (Instanz von player.tscn)
|
||||||
@@ -278,13 +295,15 @@ autoloads/ — Globaler Zustand
|
|||||||
- Eigene Systems bis Welt parallel läuft (geplant: Reparenting)
|
- Eigene Systems bis Welt parallel läuft (geplant: Reparenting)
|
||||||
|
|
||||||
## HUD (hud/)
|
## HUD (hud/)
|
||||||
- hud.tscn — CanvasLayer
|
- hud.tscn — CanvasLayer (kein Root-Skript)
|
||||||
- HealthBar (ProgressBar, Label)
|
- HealthBar (ProgressBar, Label)
|
||||||
- ShieldBar (ProgressBar, Label)
|
- ShieldBar (ProgressBar, Label)
|
||||||
- EffectContainer (HBoxContainer, programmatisch, unter ShieldBar)
|
|
||||||
- RespawnTimer (Label, Countdown bei Tod)
|
- RespawnTimer (Label, Countdown bei Tod)
|
||||||
- AbilityBar (HBoxContainer, RoleIcon + Abilities 1-4 + Passive)
|
- AbilityBar (HBoxContainer, RoleIcon + Abilities 1-4 + Passive)
|
||||||
- hud.gd — Reagiert auf Events, liest Werte von Stats, zeigt Effekt-Icons
|
- HudVitals (Node, hud_vitals.gd) — HP/Shield-Bars
|
||||||
|
- HudRespawn (Node, hud_respawn.gd) — Respawn-Timer
|
||||||
|
- HudAbilities (Node, hud_abilities.gd) — Ability-Bar + Cooldowns + Rollen-Icon
|
||||||
|
- HudEffects (Node, hud_effects.gd) — Effekt-Icons (nutzt EffectIconFactory)
|
||||||
|
|
||||||
# Abilities (Werte)
|
# Abilities (Werte)
|
||||||
- Schadens-Klasse:
|
- Schadens-Klasse:
|
||||||
@@ -308,3 +327,6 @@ autoloads/ — Globaler Zustand
|
|||||||
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
|
- 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD
|
||||||
- 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD
|
- 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD
|
||||||
- 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD)
|
- 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|||||||
72
plan2.md
Normal file
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]
|
[autoload]
|
||||||
|
|
||||||
EventBus="*res://autoloads/event_bus.gd"
|
EventBus="*res://autoloads/event_bus.gd"
|
||||||
Stats="*res://autoloads/stats/stats.gd"
|
PlayerData="*res://autoloads/player_stats.gd"
|
||||||
GameState="*res://autoloads/game_state.gd"
|
EnemyData="*res://autoloads/enemy_stats.gd"
|
||||||
|
BossData="*res://autoloads/boss_stats.gd"
|
||||||
|
PortalData="*res://autoloads/portal_stats.gd"
|
||||||
|
|
||||||
[dotnet]
|
[dotnet]
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,34 @@
|
|||||||
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"]
|
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
|
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"]
|
[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"]
|
[ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"]
|
||||||
[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"]
|
[ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
|
[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"]
|
||||||
[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
|
|
||||||
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
|
|
||||||
[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"]
|
[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
|
||||||
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
|
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
|
||||||
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
|
||||||
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
|
||||||
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
|
||||||
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
|
||||||
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
|
||||||
[ext_resource type="Script" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"]
|
[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"]
|
||||||
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"]
|
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"]
|
||||||
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
|
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
|
||||||
[ext_resource type="Script" path="res://systems/effect_system.gd" id="effect_system"]
|
[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"]
|
||||||
[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"]
|
[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"]
|
||||||
|
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"]
|
||||||
|
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"]
|
||||||
|
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"]
|
||||||
|
|
||||||
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
|
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
|
||||||
vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0)
|
vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0)
|
||||||
@@ -58,18 +69,33 @@ size = Vector3(0.5, 3, 90)
|
|||||||
[node name="HealthSystem" type="Node" parent="Systems"]
|
[node name="HealthSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("health_system")
|
script = ExtResource("health_system")
|
||||||
|
|
||||||
|
[node name="DamageSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("damage_system")
|
||||||
|
|
||||||
|
[node name="HealSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("heal_system")
|
||||||
|
|
||||||
[node name="ShieldSystem" type="Node" parent="Systems"]
|
[node name="ShieldSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("shield_system")
|
script = ExtResource("shield_system")
|
||||||
|
|
||||||
[node name="DamageSystem" type="Node" parent="Systems"]
|
[node name="RoleSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("damage_system")
|
script = ExtResource("role_system")
|
||||||
|
tank_set = ExtResource("tank_set")
|
||||||
|
damage_set = ExtResource("damage_set")
|
||||||
|
healer_set = ExtResource("healer_set")
|
||||||
|
|
||||||
[node name="AbilitySystem" type="Node" parent="Systems"]
|
[node name="AbilitySystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("ability_system")
|
script = ExtResource("ability_system")
|
||||||
|
|
||||||
|
[node name="AttackSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("attack_system")
|
||||||
|
|
||||||
[node name="CooldownSystem" type="Node" parent="Systems"]
|
[node name="CooldownSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("cooldown_system")
|
script = ExtResource("cooldown_system")
|
||||||
|
|
||||||
|
[node name="TargetingSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("targeting_system")
|
||||||
|
|
||||||
[node name="AggroSystem" type="Node" parent="Systems"]
|
[node name="AggroSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("aggro_system")
|
script = ExtResource("aggro_system")
|
||||||
|
|
||||||
@@ -82,8 +108,8 @@ script = ExtResource("aggro_decay")
|
|||||||
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
|
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
|
||||||
script = ExtResource("aggro_events")
|
script = ExtResource("aggro_events")
|
||||||
|
|
||||||
[node name="EnemyAISystem" type="Node" parent="Systems"]
|
[node name="AISystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("enemy_ai_system")
|
script = ExtResource("ai_system")
|
||||||
|
|
||||||
[node name="RespawnSystem" type="Node" parent="Systems"]
|
[node name="RespawnSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("respawn_system")
|
script = ExtResource("respawn_system")
|
||||||
@@ -91,12 +117,24 @@ script = ExtResource("respawn_system")
|
|||||||
[node name="SpawnSystem" type="Node" parent="Systems"]
|
[node name="SpawnSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("spawn_system")
|
script = ExtResource("spawn_system")
|
||||||
|
|
||||||
[node name="EffectSystem" type="Node" parent="Systems"]
|
[node name="AuraSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("effect_system")
|
script = ExtResource("aura_system")
|
||||||
|
|
||||||
|
[node name="BuffSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("buff_system")
|
||||||
|
|
||||||
|
[node name="DebuffSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("debuff_system")
|
||||||
|
|
||||||
[node name="ElementSystem" type="Node" parent="Systems"]
|
[node name="ElementSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("element_system")
|
script = ExtResource("element_system")
|
||||||
|
|
||||||
|
[node name="HudSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("hud_system")
|
||||||
|
|
||||||
|
[node name="NameplateSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("nameplate_system")
|
||||||
|
|
||||||
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
|
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
|
||||||
navigation_mesh = SubResource("NavigationMesh_1")
|
navigation_mesh = SubResource("NavigationMesh_1")
|
||||||
|
|
||||||
@@ -203,13 +241,14 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60)
|
|||||||
[node name="Enemy4d" parent="." instance=ExtResource("enemy")]
|
[node name="Enemy4d" parent="." instance=ExtResource("enemy")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
|
||||||
|
|
||||||
[node name="Boss" parent="." instance=ExtResource("boss")]
|
[node name="Boss" parent="." groups=["boss"] instance=ExtResource("enemy")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75)
|
||||||
|
stats = ExtResource("boss_stats")
|
||||||
|
|
||||||
[node name="ExitGate" parent="." instance=ExtResource("gate")]
|
[node name="ExitGate" parent="." instance=ExtResource("gate")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
|
||||||
target_scene = "res://scenes/world/world.tscn"
|
target_scene = "res://scenes/world/world.tscn"
|
||||||
is_exit = true
|
is_exit = true
|
||||||
|
|
||||||
[node name="DungeonManager" type="Node" parent="."]
|
[node name="DungeonSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("dungeon_manager")
|
script = ExtResource("dungeon_system")
|
||||||
|
|||||||
@@ -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" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="4"]
|
[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"]
|
||||||
[ext_resource type="Script" path="res://scenes/enemy/enemy_movement.gd" id="5"]
|
[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"]
|
||||||
[ext_resource type="Resource" path="res://scenes/enemy/enemy_stats.tres" id="8"]
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
height = 1.5
|
height = 1.5
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
|
||||||
|
albedo_color = Color(0.8, 0.1, 0.1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="SphereMesh_1"]
|
||||||
|
material = SubResource("StandardMaterial3D_1")
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
height = 1.5
|
height = 1.5
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
||||||
|
radius = 10.0
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
|
||||||
bg_color = Color(0.3, 0.1, 0.1, 1)
|
bg_color = Color(0.3, 0.1, 0.1, 1)
|
||||||
|
|
||||||
@@ -25,74 +33,58 @@ bg_color = Color(0.1, 0.1, 0.3, 1)
|
|||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
|
||||||
bg_color = Color(0.2, 0.5, 0.9, 1)
|
bg_color = Color(0.2, 0.5, 0.9, 1)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
|
[node name="Enemy" type="CharacterBody3D" unique_id=1724620529]
|
||||||
albedo_color = Color(0.8, 0.1, 0.1, 1)
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="SphereMesh_1"]
|
|
||||||
radius = 0.5
|
|
||||||
height = 1.0
|
|
||||||
material = SubResource("StandardMaterial3D_1")
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
|
||||||
radius = 10.0
|
|
||||||
|
|
||||||
[node name="Enemy" type="CharacterBody3D"]
|
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
stats = ExtResource("8")
|
stats = ExtResource("8")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038]
|
||||||
shape = SubResource("CapsuleShape3D_1")
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
|
|
||||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1598094615]
|
||||||
mesh = SubResource("SphereMesh_1")
|
mesh = SubResource("SphereMesh_1")
|
||||||
|
|
||||||
[node name="HitArea" type="Area3D" parent="."]
|
[node name="HitArea" type="Area3D" parent="." unique_id=893463784]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962]
|
||||||
shape = SubResource("CapsuleShape3D_2")
|
shape = SubResource("CapsuleShape3D_2")
|
||||||
|
|
||||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945]
|
||||||
|
|
||||||
[node name="EnemyMovement" type="Node" parent="."]
|
[node name="Detection" type="Node" parent="." unique_id=534240144]
|
||||||
script = ExtResource("5")
|
script = ExtResource("2")
|
||||||
|
|
||||||
[node name="DetectionArea" type="Area3D" parent="."]
|
[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 1
|
|
||||||
monitoring = true
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347]
|
||||||
shape = SubResource("SphereShape3D_1")
|
shape = SubResource("SphereShape3D_1")
|
||||||
|
|
||||||
[node name="Healthbar" type="Sprite3D" parent="."]
|
[node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
billboard = 1
|
billboard = 1
|
||||||
pixel_size = 0.01
|
|
||||||
script = ExtResource("4")
|
|
||||||
|
|
||||||
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
|
[node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718]
|
||||||
transparent_bg = true
|
transparent_bg = true
|
||||||
size = Vector2i(104, 29)
|
size = Vector2i(104, 29)
|
||||||
|
|
||||||
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"]
|
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848]
|
||||||
offset_right = 104.0
|
offset_right = 104.0
|
||||||
offset_bottom = 29.0
|
offset_bottom = 29.0
|
||||||
color = Color(1, 0.9, 0.2, 1)
|
color = Color(1, 0.9, 0.2, 1)
|
||||||
|
|
||||||
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"]
|
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403]
|
||||||
offset_left = 2.0
|
offset_left = 2.0
|
||||||
offset_top = 2.0
|
offset_top = 2.0
|
||||||
offset_right = 102.0
|
offset_right = 102.0
|
||||||
offset_bottom = 12.0
|
offset_bottom = 12.0
|
||||||
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
|
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
|
||||||
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
|
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
|
||||||
max_value = 100.0
|
|
||||||
value = 100.0
|
value = 100.0
|
||||||
show_percentage = false
|
show_percentage = false
|
||||||
|
|
||||||
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport"]
|
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036]
|
||||||
offset_left = 2.0
|
offset_left = 2.0
|
||||||
offset_top = 15.0
|
offset_top = 15.0
|
||||||
offset_right = 102.0
|
offset_right = 102.0
|
||||||
@@ -103,5 +95,5 @@ max_value = 50.0
|
|||||||
value = 50.0
|
value = 50.0
|
||||||
show_percentage = false
|
show_percentage = false
|
||||||
|
|
||||||
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
|
[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"]
|
||||||
[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"]
|
[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"]
|
||||||
|
|||||||
@@ -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]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"]
|
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"]
|
||||||
bg_color = Color(0.2, 0.2, 0.2, 0.8)
|
bg_color = Color(0.2, 0.2, 0.2, 0.8)
|
||||||
border_width_bottom = 2
|
border_width_bottom = 2
|
||||||
@@ -34,8 +32,7 @@ bg_color = Color(0.1, 0.1, 0.3, 1)
|
|||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
|
||||||
bg_color = Color(0.2, 0.5, 0.9, 1)
|
bg_color = Color(0.2, 0.5, 0.9, 1)
|
||||||
|
|
||||||
[node name="HUD" type="CanvasLayer"]
|
[node name="HUD" type="CanvasLayer" groups=["hud"]]
|
||||||
script = ExtResource("1")
|
|
||||||
|
|
||||||
[node name="HealthBar" type="ProgressBar" parent="."]
|
[node name="HealthBar" type="ProgressBar" parent="."]
|
||||||
offset_left = 10.0
|
offset_left = 10.0
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ extends Node
|
|||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
if event.is_action_pressed("ability_%s" % (i + 1)):
|
if event.is_action_pressed("ability_%s" % (i + 1)):
|
||||||
EventBus.ability_use_requested.emit(player, i)
|
EventBus.ability_use.emit(player, i)
|
||||||
return
|
return
|
||||||
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():
|
if not player.is_on_floor():
|
||||||
player.velocity.y -= gravity * delta
|
player.velocity.y -= gravity * delta
|
||||||
|
|
||||||
var base: BaseStats = Stats.get_base(player)
|
|
||||||
var speed: float = base.speed if base is PlayerStats else 5.0
|
|
||||||
var jump_velocity: float = base.jump_velocity if base is PlayerStats else 4.5
|
|
||||||
|
|
||||||
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
|
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
|
||||||
player.velocity.y = jump_velocity
|
player.velocity.y = PlayerData.jump_velocity
|
||||||
|
|
||||||
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||||
var camera_pivot := player.get_node("CameraPivot") as Node3D
|
var camera_pivot := player.get_node("CameraPivot") as Node3D
|
||||||
@@ -27,10 +23,10 @@ func _physics_process(delta: float) -> void:
|
|||||||
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
|
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
|
||||||
|
|
||||||
if direction:
|
if direction:
|
||||||
player.velocity.x = direction.x * speed
|
player.velocity.x = direction.x * PlayerData.speed
|
||||||
player.velocity.z = direction.z * speed
|
player.velocity.z = direction.z * PlayerData.speed
|
||||||
else:
|
else:
|
||||||
player.velocity.x = move_toward(player.velocity.x, 0, speed)
|
player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed)
|
||||||
player.velocity.z = move_toward(player.velocity.z, 0, speed)
|
player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed)
|
||||||
|
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
|
|||||||
@@ -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"]
|
[gd_scene format=3 uid="uid://cdnkbt1f0db7e"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bfpt2p7uucfyb" path="res://scenes/player/player.gd" id="1"]
|
[ext_resource type="Script" uid="uid://cx6k5473yxno" path="res://scenes/player/init.gd" id="1"]
|
||||||
[ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"]
|
[ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"]
|
||||||
[ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"]
|
[ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"]
|
||||||
[ext_resource type="Script" uid="uid://d15til6fsxw5b" path="res://scenes/player/combat.gd" id="4"]
|
[ext_resource type="Script" uid="uid://hh5yw7vcjdqr" path="res://scenes/player/ability.gd" id="4"]
|
||||||
[ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"]
|
[ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"]
|
||||||
[ext_resource type="Script" path="res://scenes/player/role/role.gd" id="10"]
|
[ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"]
|
||||||
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="11"]
|
[ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"]
|
||||||
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="12"]
|
|
||||||
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="13"]
|
|
||||||
[ext_resource type="Resource" path="res://scenes/player/player_stats.tres" id="14"]
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||||
radius = 0.3
|
radius = 0.3
|
||||||
@@ -19,34 +16,31 @@ height = 1.8
|
|||||||
radius = 0.3
|
radius = 0.3
|
||||||
height = 1.8
|
height = 1.8
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody3D" unique_id=1350215040]
|
[node name="Player" type="CharacterBody3D" unique_id=197716516]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
stats = ExtResource("14")
|
stats = ExtResource("14")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=311205642]
|
||||||
shape = SubResource("CapsuleShape3D_1")
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
|
|
||||||
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1346931672]
|
[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1514179122]
|
||||||
mesh = SubResource("CapsuleMesh_1")
|
mesh = SubResource("CapsuleMesh_1")
|
||||||
|
|
||||||
[node name="CameraPivot" type="Node3D" parent="." unique_id=1292689540]
|
[node name="CameraPivot" type="Node3D" parent="." unique_id=1881685457]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
script = ExtResource("2")
|
script = ExtResource("2")
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=1225820651]
|
[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=2062990383]
|
||||||
transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5)
|
transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5)
|
||||||
|
|
||||||
[node name="Movement" type="Node" parent="." unique_id=654979387]
|
[node name="Movement" type="Node" parent="." unique_id=811179177]
|
||||||
script = ExtResource("3")
|
script = ExtResource("3")
|
||||||
|
|
||||||
[node name="Combat" type="Node" parent="." unique_id=1754235583]
|
[node name="Ability" type="Node" parent="." unique_id=1184596245]
|
||||||
script = ExtResource("4")
|
script = ExtResource("4")
|
||||||
|
|
||||||
[node name="Targeting" type="Node" parent="." unique_id=592540710]
|
[node name="Targeting" type="Node" parent="." unique_id=1974574662]
|
||||||
script = ExtResource("8")
|
script = ExtResource("8")
|
||||||
|
|
||||||
[node name="Role" type="Node" parent="." unique_id=134158295]
|
[node name="Role" type="Node" parent="." unique_id=1637643687]
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
tank_set = ExtResource("11")
|
|
||||||
damage_set = ExtResource("12")
|
|
||||||
healer_set = ExtResource("13")
|
|
||||||
|
|||||||
@@ -1,40 +1,14 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
enum Role { TANK, DAMAGE, HEALER }
|
|
||||||
|
|
||||||
var current_role: int = Role.DAMAGE
|
|
||||||
|
|
||||||
@export var tank_set: AbilitySet
|
|
||||||
@export var damage_set: AbilitySet
|
|
||||||
@export var healer_set: AbilitySet
|
|
||||||
|
|
||||||
@onready var player: CharacterBody3D = get_parent()
|
@onready var player: CharacterBody3D = get_parent()
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_role.call_deferred(current_role)
|
EventBus.role_change_requested.emit(player, PlayerData.current_role)
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("class_tank"):
|
if event.is_action_pressed("class_tank"):
|
||||||
set_role(Role.TANK)
|
EventBus.role_change_requested.emit(player, PlayerData.Role.TANK)
|
||||||
elif event.is_action_pressed("class_damage"):
|
elif event.is_action_pressed("class_damage"):
|
||||||
set_role(Role.DAMAGE)
|
EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE)
|
||||||
elif event.is_action_pressed("class_healer"):
|
elif event.is_action_pressed("class_healer"):
|
||||||
set_role(Role.HEALER)
|
EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER)
|
||||||
|
|
||||||
func set_role(new_role: int) -> void:
|
|
||||||
current_role = new_role
|
|
||||||
EventBus.role_changed.emit(player, current_role)
|
|
||||||
|
|
||||||
func get_role_icon() -> String:
|
|
||||||
match current_role:
|
|
||||||
Role.TANK: return "T"
|
|
||||||
Role.DAMAGE: return "D"
|
|
||||||
Role.HEALER: return "H"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
func get_ability_set() -> AbilitySet:
|
|
||||||
match current_role:
|
|
||||||
Role.TANK: return tank_set
|
|
||||||
Role.DAMAGE: return damage_set
|
|
||||||
Role.HEALER: return healer_set
|
|
||||||
return damage_set
|
|
||||||
|
|||||||
@@ -1,27 +1,12 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const TARGET_RANGE := 20.0
|
const TARGET_RANGE := 20.0
|
||||||
const COMBAT_TIMEOUT := 3.0
|
|
||||||
|
|
||||||
var current_target: Node3D = null
|
|
||||||
var mouse_press_pos: Vector2 = Vector2.ZERO
|
var mouse_press_pos: Vector2 = Vector2.ZERO
|
||||||
var in_combat := false
|
|
||||||
var combat_timer := 0.0
|
|
||||||
|
|
||||||
@onready var player: CharacterBody3D = get_parent()
|
@onready var player: CharacterBody3D = get_parent()
|
||||||
@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D")
|
@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D")
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
EventBus.damage_dealt.connect(_on_damage_dealt)
|
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
|
||||||
EventBus.enemy_engaged.connect(_on_enemy_engaged)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
if in_combat:
|
|
||||||
combat_timer -= delta
|
|
||||||
if combat_timer <= 0:
|
|
||||||
in_combat = false
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
if event.pressed:
|
if event.pressed:
|
||||||
@@ -44,71 +29,19 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void:
|
|||||||
var result := space.intersect_ray(query)
|
var result := space.intersect_ray(query)
|
||||||
if result:
|
if result:
|
||||||
var hit_target := result.collider.get_parent() as Node3D
|
var hit_target := result.collider.get_parent() as Node3D
|
||||||
set_target(hit_target)
|
EventBus.target_requested.emit(player, hit_target)
|
||||||
else:
|
else:
|
||||||
set_target(null)
|
EventBus.target_requested.emit(player, null)
|
||||||
|
|
||||||
func _cycle_target() -> void:
|
func _cycle_target() -> void:
|
||||||
var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
|
var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
|
||||||
if targets.is_empty():
|
if targets.is_empty():
|
||||||
set_target(null)
|
EventBus.target_requested.emit(player, null)
|
||||||
return
|
return
|
||||||
if current_target == null or current_target not in targets:
|
var current: Node3D = PlayerData.target
|
||||||
set_target(targets[0])
|
if current == null or current not in targets:
|
||||||
|
EventBus.target_requested.emit(player, targets[0])
|
||||||
return
|
return
|
||||||
var idx := targets.find(current_target)
|
var idx := targets.find(current)
|
||||||
var next_idx := (idx + 1) % targets.size()
|
var next_idx := (idx + 1) % targets.size()
|
||||||
set_target(targets[next_idx])
|
EventBus.target_requested.emit(player, targets[next_idx])
|
||||||
|
|
||||||
func set_target(target: Node3D) -> void:
|
|
||||||
current_target = target
|
|
||||||
EventBus.target_changed.emit(player, target)
|
|
||||||
|
|
||||||
func _on_enemy_engaged(_enemy: Node, target: Node) -> void:
|
|
||||||
if target == player:
|
|
||||||
combat_timer = COMBAT_TIMEOUT
|
|
||||||
if not in_combat:
|
|
||||||
in_combat = true
|
|
||||||
if current_target == null:
|
|
||||||
_auto_target()
|
|
||||||
|
|
||||||
func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void:
|
|
||||||
if target == player:
|
|
||||||
combat_timer = COMBAT_TIMEOUT
|
|
||||||
if not in_combat:
|
|
||||||
in_combat = true
|
|
||||||
if current_target == null:
|
|
||||||
_auto_target()
|
|
||||||
elif attacker == player and in_combat:
|
|
||||||
combat_timer = COMBAT_TIMEOUT
|
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
|
||||||
if entity == current_target:
|
|
||||||
set_target(null)
|
|
||||||
if in_combat:
|
|
||||||
_auto_target(entity)
|
|
||||||
|
|
||||||
func _auto_target(exclude: Node = null) -> void:
|
|
||||||
# Priorität 1: Nächster Gegner
|
|
||||||
var enemies := get_tree().get_nodes_in_group("enemies")
|
|
||||||
var nearest: Node3D = null
|
|
||||||
var nearest_dist: float = INF
|
|
||||||
for enemy in enemies:
|
|
||||||
if is_instance_valid(enemy) and enemy != exclude:
|
|
||||||
var dist: float = player.global_position.distance_to(enemy.global_position)
|
|
||||||
if dist < nearest_dist:
|
|
||||||
nearest_dist = dist
|
|
||||||
nearest = enemy
|
|
||||||
if nearest:
|
|
||||||
set_target(nearest)
|
|
||||||
return
|
|
||||||
# Priorität 2: Nächstes Portal
|
|
||||||
var portals := get_tree().get_nodes_in_group("portals")
|
|
||||||
for p in portals:
|
|
||||||
if is_instance_valid(p) and p != exclude:
|
|
||||||
var dist: float = player.global_position.distance_to(p.global_position)
|
|
||||||
if dist < nearest_dist:
|
|
||||||
nearest_dist = dist
|
|
||||||
nearest = p
|
|
||||||
if nearest:
|
|
||||||
set_target(nearest)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ var active := false
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if not is_exit:
|
if not is_exit:
|
||||||
if GameState.dungeon_cleared:
|
if PlayerData.dungeon_cleared:
|
||||||
queue_free()
|
queue_free()
|
||||||
return
|
return
|
||||||
get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
|
get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
|
||||||
@@ -23,11 +23,11 @@ func _on_gate_area_body_entered(body: Node3D) -> void:
|
|||||||
if not active:
|
if not active:
|
||||||
return
|
return
|
||||||
if body is CharacterBody3D and body.name == "Player":
|
if body is CharacterBody3D and body.name == "Player":
|
||||||
GameState.save_player(body)
|
PlayerData.save_cache()
|
||||||
if is_exit:
|
if is_exit:
|
||||||
GameState.returning_from_dungeon = true
|
PlayerData.returning_from_dungeon = true
|
||||||
else:
|
else:
|
||||||
GameState.portal_position = global_position
|
PlayerData.portal_position = global_position
|
||||||
call_deferred("_change_scene")
|
call_deferred("_change_scene")
|
||||||
|
|
||||||
func _change_scene() -> void:
|
func _change_scene() -> void:
|
||||||
|
|||||||
14
scenes/portal/init.gd
Normal file
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]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scenes/portal/portal.gd" id="1"]
|
[ext_resource type="Script" path="res://scenes/portal/init.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://scenes/healthbar.gd" id="3"]
|
|
||||||
[ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"]
|
[ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
||||||
@@ -59,7 +58,6 @@ shape = SubResource("SphereShape3D_1")
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
|
||||||
billboard = 1
|
billboard = 1
|
||||||
pixel_size = 0.01
|
pixel_size = 0.01
|
||||||
script = ExtResource("3")
|
|
||||||
|
|
||||||
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
|
[node name="SubViewport" type="SubViewport" parent="Healthbar"]
|
||||||
transparent_bg = true
|
transparent_bg = true
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ var portals: Array[Node] = []
|
|||||||
var timer := 0.0
|
var timer := 0.0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if GameState.portal_position != Vector3.ZERO and not GameState.dungeon_cleared:
|
if PlayerData.portal_position != Vector3.ZERO and not PlayerData.dungeon_cleared:
|
||||||
call_deferred("_restore_gate")
|
call_deferred("_restore_gate")
|
||||||
else:
|
else:
|
||||||
if GameState.dungeon_cleared:
|
if PlayerData.dungeon_cleared:
|
||||||
GameState.clear()
|
PlayerData.clear_cache()
|
||||||
call_deferred("_spawn_portal")
|
call_deferred("_spawn_portal")
|
||||||
|
|
||||||
func _restore_gate() -> void:
|
func _restore_gate() -> void:
|
||||||
var gate: Node3D = GATE_SCENE.instantiate()
|
var gate: Node3D = GATE_SCENE.instantiate()
|
||||||
get_parent().add_child(gate)
|
get_parent().add_child(gate)
|
||||||
gate.global_position = GameState.portal_position
|
gate.global_position = PlayerData.portal_position
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
timer += delta
|
timer += delta
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
[gd_scene format=3 uid="uid://dy1icabu2ssbw"]
|
[gd_scene format=3 uid="uid://dy1icabu2ssbw"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://h0hts425epc6" path="res://systems/ability_system.gd" id="ability_system"]
|
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
|
||||||
[ext_resource type="Script" uid="uid://cysg30lud2ta2" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
|
||||||
[ext_resource type="Script" uid="uid://cyffo1g4uhmwh" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
|
||||||
[ext_resource type="Script" uid="uid://cm7ehl2pexcst" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
|
||||||
[ext_resource type="Script" uid="uid://c7gsu2qddsor6" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
|
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
|
||||||
[ext_resource type="Script" uid="uid://ddos7mo8rahou" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
|
||||||
[ext_resource type="Script" uid="uid://cbd1bryh0e2dw" path="res://systems/damage_system.gd" id="damage_system"]
|
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
|
||||||
[ext_resource type="Script" uid="uid://drdlh6tq0dfwo" path="res://systems/effect_system.gd" id="effect_system"]
|
[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"]
|
||||||
[ext_resource type="Script" uid="uid://bqebxfvticxto" path="res://systems/element_system.gd" id="element_system"]
|
[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"]
|
||||||
[ext_resource type="Script" uid="uid://bwhxu5586lc1l" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"]
|
[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
|
||||||
[ext_resource type="Script" uid="uid://b3wkn5118dimy" path="res://systems/health_system.gd" id="health_system"]
|
[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/portal_system.gd" id="portal_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
|
||||||
|
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
|
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"]
|
[ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"]
|
||||||
[ext_resource type="Script" uid="uid://cskx6o07iukwh" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"]
|
[ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"]
|
||||||
[ext_resource type="Script" uid="uid://b1qkvoqvmd21h" path="res://systems/respawn_system.gd" id="respawn_system"]
|
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"]
|
||||||
[ext_resource type="Script" uid="uid://rsnpuf77o0sn" path="res://systems/shield_system.gd" id="shield_system"]
|
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"]
|
||||||
[ext_resource type="Script" uid="uid://c84voxmnaifyt" path="res://systems/spawn_system.gd" id="spawn_system"]
|
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"]
|
||||||
|
|
||||||
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
|
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
|
||||||
vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5)
|
vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5)
|
||||||
@@ -53,81 +65,111 @@ size = Vector3(5, 3, 5)
|
|||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"]
|
||||||
size = Vector3(5, 3, 5)
|
size = Vector3(5, 3, 5)
|
||||||
|
|
||||||
[node name="World" type="Node3D" unique_id=1865233338]
|
[node name="World" type="Node3D"]
|
||||||
|
|
||||||
[node name="Systems" type="Node" parent="." unique_id=1813416478]
|
[node name="Systems" type="Node" parent="."]
|
||||||
|
|
||||||
[node name="HealthSystem" type="Node" parent="Systems" unique_id=221270411]
|
[node name="HealthSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("health_system")
|
script = ExtResource("health_system")
|
||||||
|
|
||||||
[node name="ShieldSystem" type="Node" parent="Systems" unique_id=1790230220]
|
[node name="DamageSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("shield_system")
|
|
||||||
|
|
||||||
[node name="DamageSystem" type="Node" parent="Systems" unique_id=2146323526]
|
|
||||||
script = ExtResource("damage_system")
|
script = ExtResource("damage_system")
|
||||||
|
|
||||||
[node name="AbilitySystem" type="Node" parent="Systems" unique_id=391120092]
|
[node name="HealSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("heal_system")
|
||||||
|
|
||||||
|
[node name="ShieldSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("shield_system")
|
||||||
|
|
||||||
|
[node name="RoleSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("role_system")
|
||||||
|
tank_set = ExtResource("tank_set")
|
||||||
|
damage_set = ExtResource("damage_set")
|
||||||
|
healer_set = ExtResource("healer_set")
|
||||||
|
|
||||||
|
[node name="AbilitySystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("ability_system")
|
script = ExtResource("ability_system")
|
||||||
|
|
||||||
[node name="CooldownSystem" type="Node" parent="Systems" unique_id=99457358]
|
[node name="AttackSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("attack_system")
|
||||||
|
|
||||||
|
[node name="CooldownSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("cooldown_system")
|
script = ExtResource("cooldown_system")
|
||||||
|
|
||||||
[node name="AggroSystem" type="Node" parent="Systems" unique_id=1539448343]
|
[node name="TargetingSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("targeting_system")
|
||||||
|
|
||||||
|
[node name="AggroSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("aggro_system")
|
script = ExtResource("aggro_system")
|
||||||
|
|
||||||
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem" unique_id=1597893665]
|
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"]
|
||||||
script = ExtResource("aggro_tracker")
|
script = ExtResource("aggro_tracker")
|
||||||
|
|
||||||
[node name="AggroDecay" type="Node" parent="Systems/AggroSystem" unique_id=1571705506]
|
[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"]
|
||||||
script = ExtResource("aggro_decay")
|
script = ExtResource("aggro_decay")
|
||||||
|
|
||||||
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem" unique_id=1936723580]
|
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
|
||||||
script = ExtResource("aggro_events")
|
script = ExtResource("aggro_events")
|
||||||
|
|
||||||
[node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042]
|
[node name="AISystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("enemy_ai_system")
|
script = ExtResource("ai_system")
|
||||||
|
|
||||||
[node name="RespawnSystem" type="Node" parent="Systems" unique_id=1586865573]
|
[node name="RespawnSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("respawn_system")
|
script = ExtResource("respawn_system")
|
||||||
|
|
||||||
[node name="SpawnSystem" type="Node" parent="Systems" unique_id=1099032666]
|
[node name="SpawnSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("spawn_system")
|
script = ExtResource("spawn_system")
|
||||||
|
|
||||||
[node name="EffectSystem" type="Node" parent="Systems" unique_id=1219368182]
|
[node name="PortalSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("effect_system")
|
script = ExtResource("portal_system")
|
||||||
|
|
||||||
[node name="ElementSystem" type="Node" parent="Systems" unique_id=1401212832]
|
[node name="AuraSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("aura_system")
|
||||||
|
|
||||||
|
[node name="BuffSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("buff_system")
|
||||||
|
|
||||||
|
[node name="DebuffSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("debuff_system")
|
||||||
|
|
||||||
|
[node name="ElementSystem" type="Node" parent="Systems"]
|
||||||
script = ExtResource("element_system")
|
script = ExtResource("element_system")
|
||||||
|
|
||||||
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679]
|
[node name="HudSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("hud_system")
|
||||||
|
|
||||||
|
[node name="NameplateSystem" type="Node" parent="Systems"]
|
||||||
|
script = ExtResource("nameplate_system")
|
||||||
|
|
||||||
|
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506)
|
||||||
navigation_mesh = SubResource("NavigationMesh_1")
|
navigation_mesh = SubResource("NavigationMesh_1")
|
||||||
|
|
||||||
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=593226019]
|
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"]
|
||||||
mesh = SubResource("PlaneMesh_1")
|
mesh = SubResource("PlaneMesh_1")
|
||||||
|
|
||||||
[node name="BodenCollision" type="StaticBody3D" parent="." unique_id=1112667638]
|
[node name="BodenCollision" type="StaticBody3D" parent="."]
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision" unique_id=621554623]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
|
||||||
shape = SubResource("WorldBoundaryShape3D_1")
|
shape = SubResource("WorldBoundaryShape3D_1")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1797472817]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
|
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
|
|
||||||
[node name="Taverne" type="StaticBody3D" parent="." unique_id=1978646562]
|
[node name="Taverne" type="StaticBody3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||||
|
|
||||||
[node name="Mesh" type="MeshInstance3D" parent="Taverne" unique_id=2043279810]
|
[node name="Mesh" type="MeshInstance3D" parent="Taverne"]
|
||||||
mesh = SubResource("BoxMesh_tavern")
|
mesh = SubResource("BoxMesh_tavern")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne" unique_id=2108564286]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne"]
|
||||||
shape = SubResource("BoxShape3D_tavern")
|
shape = SubResource("BoxShape3D_tavern")
|
||||||
|
|
||||||
[node name="Player" parent="." unique_id=585018813 instance=ExtResource("player")]
|
[node name="Player" parent="." instance=ExtResource("player")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5)
|
||||||
|
|
||||||
[node name="HUD" parent="." unique_id=763693646 instance=ExtResource("hud")]
|
[node name="HUD" parent="." instance=ExtResource("hud")]
|
||||||
|
|
||||||
[node name="PortalSpawner" type="Node" parent="." unique_id=2100621428]
|
[node name="PortalSpawner" type="Node" parent="."]
|
||||||
script = ExtResource("portal_spawner")
|
script = ExtResource("portal_spawner")
|
||||||
|
|||||||
@@ -1,124 +1,83 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
EventBus.ability_use_requested.connect(_on_ability_use_requested)
|
EventBus.ability_use.connect(_on_ability_use)
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _on_ability_use(_player: Node, ability_index: int) -> void:
|
||||||
var players := get_tree().get_nodes_in_group("player")
|
if not PlayerData.alive:
|
||||||
for player in players:
|
|
||||||
if not Stats.is_alive(player):
|
|
||||||
continue
|
|
||||||
_try_auto_attack(player)
|
|
||||||
|
|
||||||
func _try_auto_attack(player: Node) -> void:
|
|
||||||
var targeting: Node = player.get_node_or_null("Targeting")
|
|
||||||
if not targeting or not targeting.in_combat or not targeting.current_target:
|
|
||||||
return
|
return
|
||||||
if not is_instance_valid(targeting.current_target):
|
var ability_set: AbilitySet = PlayerData.ability_set
|
||||||
return
|
|
||||||
var cooldown_system: Node = get_node("../CooldownSystem")
|
|
||||||
if not cooldown_system.is_aa_ready(player):
|
|
||||||
return
|
|
||||||
var role: Node = player.get_node("Role")
|
|
||||||
var ability_set: AbilitySet = role.get_ability_set()
|
|
||||||
if not ability_set:
|
|
||||||
return
|
|
||||||
var aa_damage: float = ability_set.aa_damage
|
|
||||||
var aa_range: float = ability_set.aa_range
|
|
||||||
var aa_is_heal: bool = ability_set.aa_is_heal
|
|
||||||
var dmg: float = _apply_passive(player, aa_damage, "heal" if aa_is_heal else "damage")
|
|
||||||
if aa_is_heal:
|
|
||||||
EventBus.heal_requested.emit(player, player, dmg)
|
|
||||||
else:
|
|
||||||
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
|
|
||||||
if dist > aa_range:
|
|
||||||
return
|
|
||||||
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
|
|
||||||
var player_base: BaseStats = Stats.get_base(player)
|
|
||||||
var aa_cd: float = player_base.aa_cooldown if player_base is PlayerStats else 0.5
|
|
||||||
cooldown_system.set_aa_cooldown(player, aa_cd)
|
|
||||||
|
|
||||||
func _on_ability_use_requested(player: Node, ability_index: int) -> void:
|
|
||||||
var role: Node = player.get_node_or_null("Role")
|
|
||||||
if not role:
|
|
||||||
return
|
|
||||||
var ability_set: AbilitySet = role.get_ability_set()
|
|
||||||
if not ability_set or ability_index >= ability_set.abilities.size():
|
if not ability_set or ability_index >= ability_set.abilities.size():
|
||||||
return
|
return
|
||||||
var ability: Ability = ability_set.abilities[ability_index]
|
var ability: Ability = ability_set.abilities[ability_index]
|
||||||
if not ability or ability.type == Ability.Type.PASSIVE:
|
if not ability or ability.type == Ability.Type.PASSIVE:
|
||||||
return
|
return
|
||||||
var cooldown_system: Node = get_node("../CooldownSystem")
|
if PlayerData.cooldowns[ability_index] > 0:
|
||||||
if not cooldown_system.is_ready(player, ability_index):
|
|
||||||
return
|
return
|
||||||
if ability.uses_gcd and not cooldown_system.is_gcd_ready(player):
|
if ability.uses_gcd and PlayerData.gcd > 0:
|
||||||
return
|
return
|
||||||
var success: bool = _execute_ability(player, ability)
|
var success: bool = _execute_ability(ability)
|
||||||
if not success:
|
if not success:
|
||||||
return
|
return
|
||||||
var player_base: BaseStats = Stats.get_base(player)
|
var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0
|
||||||
var gcd_time: float = player_base.gcd_time if player_base is PlayerStats else 0.5
|
PlayerData.cooldowns[ability_index] = ability.cooldown
|
||||||
var gcd: float = gcd_time if ability.uses_gcd else 0.0
|
PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd)
|
||||||
cooldown_system.set_cooldown(player, ability_index, ability.cooldown, gcd)
|
if gcd > 0:
|
||||||
|
PlayerData.gcd = gcd
|
||||||
|
|
||||||
func _execute_ability(player: Node, ability: Ability) -> bool:
|
func _execute_ability(ability: Ability) -> bool:
|
||||||
var targeting: Node = player.get_node("Targeting")
|
|
||||||
var stat: String = "heal" if ability.is_heal else "damage"
|
var stat: String = "heal" if ability.is_heal else "damage"
|
||||||
var dmg: float = _apply_passive(player, ability.damage, stat)
|
var dmg: float = _apply_passive(ability.damage, stat)
|
||||||
|
var player: Node = get_tree().get_first_node_in_group("player")
|
||||||
match ability.type:
|
match ability.type:
|
||||||
Ability.Type.SINGLE:
|
Ability.Type.SINGLE:
|
||||||
return _execute_single(player, targeting, ability, dmg)
|
return _execute_single(player, ability, dmg)
|
||||||
Ability.Type.AOE:
|
Ability.Type.AOE:
|
||||||
return _execute_aoe(player, ability, dmg)
|
return _execute_aoe(player, ability, dmg)
|
||||||
Ability.Type.UTILITY:
|
Ability.Type.UTILITY:
|
||||||
return _execute_utility(player, ability)
|
return _execute_utility(ability)
|
||||||
Ability.Type.ULT:
|
Ability.Type.ULT:
|
||||||
return _execute_ult(player, targeting, ability, dmg)
|
return _execute_ult(player, ability, dmg)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
func _apply_passive(player: Node, base: float, stat: String) -> float:
|
func _apply_passive(base: float, stat: String) -> float:
|
||||||
var mult: Variant = Stats.get_stat(player, "buff_" + stat)
|
var mult: float = 1.0
|
||||||
if mult != null:
|
match stat:
|
||||||
return base * mult
|
"damage": mult = PlayerData.buff_damage
|
||||||
return base
|
"heal": mult = PlayerData.buff_heal
|
||||||
|
return base * mult
|
||||||
|
|
||||||
func _in_range(player: Node, targeting: Node, ability: Ability) -> bool:
|
func _in_range(ability: Ability) -> bool:
|
||||||
if ability.ability_range <= 0 or ability.is_heal:
|
if ability.ability_range <= 0 or ability.is_heal:
|
||||||
return true
|
return true
|
||||||
if not is_instance_valid(targeting.current_target):
|
if not is_instance_valid(PlayerData.target):
|
||||||
return false
|
return false
|
||||||
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
|
var player: Node = get_tree().get_first_node_in_group("player")
|
||||||
|
var dist: float = player.global_position.distance_to(PlayerData.target.global_position)
|
||||||
return dist <= ability.ability_range
|
return dist <= ability.ability_range
|
||||||
|
|
||||||
func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool:
|
func _execute_single(player: Node, ability: Ability, dmg: float) -> bool:
|
||||||
if ability.is_heal:
|
if ability.is_heal:
|
||||||
EventBus.heal_requested.emit(player, player, dmg)
|
EventBus.heal_requested.emit(player, player, dmg)
|
||||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||||
return true
|
return true
|
||||||
if not _in_range(player, targeting, ability):
|
if not _in_range(ability):
|
||||||
return false
|
return false
|
||||||
if not is_instance_valid(targeting.current_target):
|
if not is_instance_valid(PlayerData.target):
|
||||||
return false
|
return false
|
||||||
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
|
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
|
||||||
if ability.element != 0:
|
if ability.element != 0:
|
||||||
EventBus.element_damage_dealt.emit(player, targeting.current_target, dmg, ability.element)
|
EventBus.element_damage_dealt.emit(player, PlayerData.target, dmg, ability.element)
|
||||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
|
func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
|
||||||
if ability.is_heal:
|
if ability.is_heal:
|
||||||
EventBus.heal_requested.emit(player, player, dmg)
|
EventBus.heal_requested.emit(player, player, dmg)
|
||||||
var players := get_tree().get_nodes_in_group("player")
|
|
||||||
for p in players:
|
|
||||||
if p != player and is_instance_valid(p):
|
|
||||||
var dist: float = player.global_position.distance_to(p.global_position)
|
|
||||||
if dist <= ability.ability_range:
|
|
||||||
EventBus.heal_requested.emit(player, p, dmg)
|
|
||||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||||
return true
|
return true
|
||||||
var hit := false
|
var hit := false
|
||||||
var enemies := get_tree().get_nodes_in_group("enemies")
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
for enemy in enemies:
|
|
||||||
var dist: float = player.global_position.distance_to(enemy.global_position)
|
var dist: float = player.global_position.distance_to(enemy.global_position)
|
||||||
if dist <= ability.ability_range:
|
if dist <= ability.ability_range:
|
||||||
EventBus.damage_requested.emit(player, enemy, dmg)
|
EventBus.damage_requested.emit(player, enemy, dmg)
|
||||||
@@ -129,44 +88,35 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
|
|||||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||||
return hit
|
return hit
|
||||||
|
|
||||||
func _execute_utility(player: Node, ability: Ability) -> bool:
|
func _execute_utility(ability: Ability) -> bool:
|
||||||
var max_shield: float = Stats.get_stat(player, "max_shield")
|
if PlayerData.max_shield <= 0:
|
||||||
if max_shield <= 0:
|
|
||||||
return false
|
return false
|
||||||
var shield: float = Stats.get_stat(player, "shield")
|
var shield: float = PlayerData.shield
|
||||||
if ability.damage > 0:
|
if ability.damage > 0:
|
||||||
shield = max_shield * (ability.damage / 100.0)
|
shield = PlayerData.max_shield * (ability.damage / 100.0)
|
||||||
else:
|
else:
|
||||||
if shield >= max_shield:
|
if shield >= PlayerData.max_shield:
|
||||||
return false
|
return false
|
||||||
shield = max_shield
|
shield = PlayerData.max_shield
|
||||||
Stats.set_stat(player, "shield", shield)
|
PlayerData.set_shield(shield)
|
||||||
EventBus.shield_changed.emit(player, shield, max_shield)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool:
|
func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool:
|
||||||
if ability.is_heal:
|
if ability.is_heal:
|
||||||
EventBus.heal_requested.emit(player, player, dmg)
|
EventBus.heal_requested.emit(player, player, dmg)
|
||||||
var players := get_tree().get_nodes_in_group("player")
|
|
||||||
var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
|
var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
|
||||||
for p in players:
|
|
||||||
if p != player and is_instance_valid(p):
|
|
||||||
var dist: float = player.global_position.distance_to(p.global_position)
|
|
||||||
if dist <= aoe_range:
|
|
||||||
EventBus.heal_requested.emit(player, p, dmg * 0.4)
|
|
||||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||||
return true
|
return true
|
||||||
if not _in_range(player, targeting, ability):
|
if not _in_range(ability):
|
||||||
return false
|
return false
|
||||||
if not is_instance_valid(targeting.current_target):
|
if not is_instance_valid(PlayerData.target):
|
||||||
return false
|
return false
|
||||||
var target: Node3D = targeting.current_target
|
var target: Node3D = PlayerData.target
|
||||||
EventBus.damage_requested.emit(player, target, dmg * 5.0)
|
EventBus.damage_requested.emit(player, target, dmg * 5.0)
|
||||||
if ability.element != 0:
|
if ability.element != 0:
|
||||||
EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element)
|
EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element)
|
||||||
var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
|
var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
|
||||||
var enemies := get_tree().get_nodes_in_group("enemies")
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
for enemy in enemies:
|
|
||||||
if enemy != target and is_instance_valid(enemy):
|
if enemy != target and is_instance_valid(enemy):
|
||||||
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
|
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
|
||||||
if enemy_dist <= splash_range:
|
if enemy_dist <= splash_range:
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ func _update_combat_timers(delta: float) -> void:
|
|||||||
|
|
||||||
func _decay_aggro(enemy: Node, delta: float) -> void:
|
func _decay_aggro(enemy: Node, delta: float) -> void:
|
||||||
var table: Dictionary = tracker.aggro_tables[enemy]
|
var table: Dictionary = tracker.aggro_tables[enemy]
|
||||||
var base: BaseStats = Stats.get_base(enemy)
|
var data_source: Node = tracker._get_data_source(enemy)
|
||||||
var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0
|
var aggro_decay: float = 1.0
|
||||||
|
if data_source:
|
||||||
|
var base: EnemyStats = data_source.get_base(enemy)
|
||||||
|
if base:
|
||||||
|
aggro_decay = base.aggro_decay
|
||||||
for player in table.keys():
|
for player in table.keys():
|
||||||
if is_in_combat(player):
|
if is_in_combat(player):
|
||||||
continue
|
continue
|
||||||
@@ -58,10 +62,11 @@ func spread_aggro(source: Node, attacker: Node, amount: float) -> void:
|
|||||||
func alert_nearby(enemy: Node, target: Node) -> void:
|
func alert_nearby(enemy: Node, target: Node) -> void:
|
||||||
var radius: float = tracker.get_alert_radius(enemy)
|
var radius: float = tracker.get_alert_radius(enemy)
|
||||||
for other in tracker.get_enemies_in_radius(enemy, radius):
|
for other in tracker.get_enemies_in_radius(enemy, radius):
|
||||||
if "state" in other and other.state == other.State.IDLE:
|
var data_source: Node = tracker._get_data_source(other)
|
||||||
|
if data_source and data_source.get_stat(other, "state") == 0:
|
||||||
tracker.add_aggro(other, target, 1.0)
|
tracker.add_aggro(other, target, 1.0)
|
||||||
other.target = target
|
data_source.set_stat(other, "target", target)
|
||||||
other.state = other.State.CHASE
|
data_source.set_stat(other, "state", 1)
|
||||||
EventBus.enemy_engaged.emit(other, target)
|
EventBus.enemy_engaged.emit(other, target)
|
||||||
|
|
||||||
func erase_entity(entity: Node) -> void:
|
func erase_entity(entity: Node) -> void:
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ func _ready() -> void:
|
|||||||
func _on_enemy_detected(enemy: Node, player: Node) -> void:
|
func _on_enemy_detected(enemy: Node, player: Node) -> void:
|
||||||
if not enemy.is_in_group("enemies"):
|
if not enemy.is_in_group("enemies"):
|
||||||
return
|
return
|
||||||
if "state" in enemy:
|
var data_source: Node = tracker._get_data_source(enemy)
|
||||||
if enemy.state == enemy.State.CHASE or enemy.state == enemy.State.ATTACK:
|
if data_source:
|
||||||
|
var state: int = data_source.get_stat(enemy, "state")
|
||||||
|
if state == 1 or state == 2:
|
||||||
return
|
return
|
||||||
tracker.add_player_in_range(enemy, player)
|
tracker.add_player_in_range(enemy, player)
|
||||||
tracker.add_aggro(enemy, player, 1.0)
|
tracker.add_aggro(enemy, player, 1.0)
|
||||||
if "state" in enemy:
|
if data_source:
|
||||||
enemy.target = player
|
data_source.set_stat(enemy, "target", player)
|
||||||
enemy.state = enemy.State.CHASE
|
data_source.set_stat(enemy, "state", 1)
|
||||||
EventBus.enemy_engaged.emit(enemy, player)
|
EventBus.enemy_engaged.emit(enemy, player)
|
||||||
decay.alert_nearby(enemy, player)
|
decay.alert_nearby(enemy, player)
|
||||||
|
|
||||||
@@ -33,8 +35,7 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
|
|||||||
return
|
return
|
||||||
decay.reset_combat_timer(attacker)
|
decay.reset_combat_timer(attacker)
|
||||||
var multiplier := 1.0
|
var multiplier := 1.0
|
||||||
var role: Node = attacker.get_node_or_null("Role")
|
if PlayerData.current_role == PlayerData.Role.TANK:
|
||||||
if role and role.current_role == 0:
|
|
||||||
multiplier = config.tank_multiplier
|
multiplier = config.tank_multiplier
|
||||||
var aggro: float = amount * multiplier
|
var aggro: float = amount * multiplier
|
||||||
tracker.add_aggro(target, attacker, aggro)
|
tracker.add_aggro(target, attacker, aggro)
|
||||||
|
|||||||
@@ -43,17 +43,19 @@ func get_top_target(table: Dictionary) -> Node:
|
|||||||
return top
|
return top
|
||||||
|
|
||||||
func update_target(enemy: Node) -> void:
|
func update_target(enemy: Node) -> void:
|
||||||
if not "state" in enemy:
|
|
||||||
return
|
|
||||||
var table: Dictionary = aggro_tables[enemy]
|
var table: Dictionary = aggro_tables[enemy]
|
||||||
var top: Node = get_top_target(table)
|
var top: Node = get_top_target(table)
|
||||||
if top and top != enemy.target:
|
var data_source: Node = _get_data_source(enemy)
|
||||||
enemy.target = top
|
if not data_source:
|
||||||
if enemy.state == enemy.State.IDLE or enemy.state == enemy.State.RETURN:
|
return
|
||||||
enemy.state = enemy.State.CHASE
|
var state: int = data_source.get_stat(enemy, "state")
|
||||||
elif not top and enemy.state != enemy.State.IDLE and enemy.state != enemy.State.RETURN:
|
if top and top != data_source.get_stat(enemy, "target"):
|
||||||
enemy.target = null
|
data_source.set_stat(enemy, "target", top)
|
||||||
enemy.state = enemy.State.RETURN
|
if state == 0 or state == 3:
|
||||||
|
data_source.set_stat(enemy, "state", 1)
|
||||||
|
elif not top and state != 0 and state != 3:
|
||||||
|
data_source.set_stat(enemy, "target", null)
|
||||||
|
data_source.set_stat(enemy, "state", 3)
|
||||||
|
|
||||||
func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
||||||
var result: Array = []
|
var result: Array = []
|
||||||
@@ -65,8 +67,12 @@ func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
func get_alert_radius(entity: Node) -> float:
|
func get_alert_radius(entity: Node) -> float:
|
||||||
var base: BaseStats = Stats.get_base(entity)
|
var data_source: Node = _get_data_source(entity)
|
||||||
return base.alert_radius if base is EnemyStats else 10.0
|
if data_source:
|
||||||
|
var base: EnemyStats = data_source.get_base(entity)
|
||||||
|
if base:
|
||||||
|
return base.alert_radius
|
||||||
|
return 10.0
|
||||||
|
|
||||||
func erase_entity(entity: Node) -> void:
|
func erase_entity(entity: Node) -> void:
|
||||||
aggro_tables.erase(entity)
|
aggro_tables.erase(entity)
|
||||||
@@ -74,9 +80,17 @@ func erase_entity(entity: Node) -> void:
|
|||||||
for enemy in aggro_tables:
|
for enemy in aggro_tables:
|
||||||
if is_instance_valid(enemy):
|
if is_instance_valid(enemy):
|
||||||
aggro_tables[enemy].erase(entity)
|
aggro_tables[enemy].erase(entity)
|
||||||
if "target" in enemy and entity == enemy.target:
|
var data_source: Node = _get_data_source(enemy)
|
||||||
enemy.target = null
|
if data_source and data_source.get_stat(enemy, "target") == entity:
|
||||||
enemy.state = enemy.State.RETURN
|
data_source.set_stat(enemy, "target", null)
|
||||||
|
data_source.set_stat(enemy, "state", 3)
|
||||||
for enemy in players_in_range:
|
for enemy in players_in_range:
|
||||||
if is_instance_valid(enemy):
|
if is_instance_valid(enemy):
|
||||||
players_in_range[enemy].erase(entity)
|
players_in_range[enemy].erase(entity)
|
||||||
|
|
||||||
|
func _get_data_source(entity: Node) -> Node:
|
||||||
|
if entity.is_in_group("boss"):
|
||||||
|
return BossData
|
||||||
|
elif entity.is_in_group("enemies"):
|
||||||
|
return EnemyData
|
||||||
|
return null
|
||||||
|
|||||||
91
systems/ai_system.gd
Normal file
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
|
extends Node
|
||||||
|
|
||||||
var cooldowns: Dictionary = {}
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("cooldown_system")
|
|
||||||
EventBus.role_changed.connect(_on_role_changed)
|
EventBus.role_changed.connect(_on_role_changed)
|
||||||
|
|
||||||
func register(entity: Node, ability_count: int) -> void:
|
|
||||||
cooldowns[entity] = {
|
|
||||||
"cds": [] as Array[float],
|
|
||||||
"max_cds": [] as Array[float],
|
|
||||||
"gcd": 0.0,
|
|
||||||
"aa": 0.0,
|
|
||||||
}
|
|
||||||
cooldowns[entity]["cds"].resize(ability_count)
|
|
||||||
cooldowns[entity]["cds"].fill(0.0)
|
|
||||||
cooldowns[entity]["max_cds"].resize(ability_count)
|
|
||||||
cooldowns[entity]["max_cds"].fill(0.0)
|
|
||||||
|
|
||||||
func deregister(entity: Node) -> void:
|
|
||||||
cooldowns.erase(entity)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
for entity in cooldowns:
|
if PlayerData.gcd > 0:
|
||||||
if not is_instance_valid(entity):
|
PlayerData.gcd -= delta
|
||||||
continue
|
if PlayerData.aa_timer > 0:
|
||||||
var data: Dictionary = cooldowns[entity]
|
PlayerData.aa_timer -= delta
|
||||||
if data["gcd"] > 0:
|
for i in range(PlayerData.cooldowns.size()):
|
||||||
data["gcd"] -= delta
|
if PlayerData.cooldowns[i] > 0:
|
||||||
if data["aa"] > 0:
|
PlayerData.cooldowns[i] -= delta
|
||||||
data["aa"] -= delta
|
EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.gcd)
|
||||||
var cds: Array = data["cds"]
|
|
||||||
for i in range(cds.size()):
|
|
||||||
if cds[i] > 0:
|
|
||||||
cds[i] -= delta
|
|
||||||
EventBus.cooldown_tick.emit(cds, data["max_cds"], data["gcd"])
|
|
||||||
|
|
||||||
func is_ready(entity: Node, index: int) -> bool:
|
func _on_role_changed(_player: Node, _role_type: int) -> void:
|
||||||
if entity not in cooldowns:
|
PlayerData.cooldowns.fill(0.0)
|
||||||
return false
|
PlayerData.max_cooldowns.fill(0.0)
|
||||||
return cooldowns[entity]["cds"][index] <= 0
|
PlayerData.gcd = 0.0
|
||||||
|
|
||||||
func is_gcd_ready(entity: Node) -> bool:
|
|
||||||
if entity not in cooldowns:
|
|
||||||
return false
|
|
||||||
return cooldowns[entity]["gcd"] <= 0
|
|
||||||
|
|
||||||
func is_aa_ready(entity: Node) -> bool:
|
|
||||||
if entity not in cooldowns:
|
|
||||||
return false
|
|
||||||
return cooldowns[entity]["aa"] <= 0
|
|
||||||
|
|
||||||
func set_cooldown(entity: Node, index: int, cd: float, gcd: float) -> void:
|
|
||||||
if entity not in cooldowns:
|
|
||||||
return
|
|
||||||
var data: Dictionary = cooldowns[entity]
|
|
||||||
data["cds"][index] = cd
|
|
||||||
data["max_cds"][index] = max(cd, gcd)
|
|
||||||
if gcd > 0:
|
|
||||||
data["gcd"] = gcd
|
|
||||||
|
|
||||||
func set_aa_cooldown(entity: Node, cd: float) -> void:
|
|
||||||
if entity not in cooldowns:
|
|
||||||
return
|
|
||||||
cooldowns[entity]["aa"] = cd
|
|
||||||
|
|
||||||
func _on_role_changed(player: Node, _role_type: int) -> void:
|
|
||||||
if player in cooldowns:
|
|
||||||
var data: Dictionary = cooldowns[player]
|
|
||||||
data["cds"].fill(0.0)
|
|
||||||
data["max_cds"].fill(0.0)
|
|
||||||
data["gcd"] = 0.0
|
|
||||||
|
|||||||
@@ -1 +1,38 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
EventBus.damage_requested.connect(_on_damage_requested)
|
||||||
|
|
||||||
|
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
|
||||||
|
var remaining: float = amount
|
||||||
|
var shield_system: Node = get_node_or_null("../ShieldSystem")
|
||||||
|
if shield_system:
|
||||||
|
remaining = shield_system.absorb(target, remaining)
|
||||||
|
EventBus.damage_dealt.emit(attacker, target, amount)
|
||||||
|
if remaining > 0:
|
||||||
|
_apply_damage(target, remaining)
|
||||||
|
|
||||||
|
func _apply_damage(entity: Node, amount: float) -> void:
|
||||||
|
if entity == _get_player():
|
||||||
|
var health: float = PlayerData.health - amount
|
||||||
|
if health < 0:
|
||||||
|
health = 0
|
||||||
|
PlayerData.set_health(health)
|
||||||
|
elif entity.is_in_group("boss"):
|
||||||
|
var health: float = BossData.get_stat(entity, "health") - amount
|
||||||
|
if health < 0:
|
||||||
|
health = 0
|
||||||
|
BossData.set_health(entity, health)
|
||||||
|
elif entity.is_in_group("enemies"):
|
||||||
|
var health: float = EnemyData.get_stat(entity, "health") - amount
|
||||||
|
if health < 0:
|
||||||
|
health = 0
|
||||||
|
EnemyData.set_health(entity, health)
|
||||||
|
elif entity.is_in_group("portals"):
|
||||||
|
var health: float = PortalData.get_stat(entity, "health") - amount
|
||||||
|
if health < 0:
|
||||||
|
health = 0
|
||||||
|
PortalData.set_health(entity, health)
|
||||||
|
|
||||||
|
func _get_player() -> Node:
|
||||||
|
return get_tree().get_first_node_in_group("player")
|
||||||
|
|||||||
@@ -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
|
extends Node
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var player: Node = get_tree().get_first_node_in_group("player")
|
|
||||||
if player:
|
|
||||||
GameState.restore_player(player)
|
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
EventBus.entity_died.connect(_on_entity_died)
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
func _on_entity_died(entity: Node) -> void:
|
||||||
if entity.is_in_group("boss"):
|
if entity.is_in_group("boss"):
|
||||||
await get_tree().create_timer(2.0).timeout
|
await get_tree().create_timer(2.0).timeout
|
||||||
GameState.dungeon_cleared = true
|
PlayerData.dungeon_cleared = true
|
||||||
GameState.returning_from_dungeon = false
|
PlayerData.returning_from_dungeon = false
|
||||||
GameState.clear()
|
PlayerData.clear_cache()
|
||||||
EventBus.dungeon_cleared.emit()
|
EventBus.dungeon_cleared.emit()
|
||||||
get_tree().change_scene_to_file("res://scenes/world/world.tscn")
|
get_tree().change_scene_to_file("res://scenes/world/world.tscn")
|
||||||
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
|
extends Node
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
EventBus.damage_requested.connect(_on_damage_requested)
|
_emit_initial.call_deferred()
|
||||||
EventBus.heal_requested.connect(_on_heal_requested)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
for entity in Stats.entities:
|
_regen_player(delta)
|
||||||
|
_regen_entities(delta, EnemyData.entities)
|
||||||
|
_regen_entities(delta, BossData.entities)
|
||||||
|
|
||||||
|
func _regen_player(delta: float) -> void:
|
||||||
|
if not PlayerData.alive or PlayerData.health_regen <= 0:
|
||||||
|
return
|
||||||
|
if PlayerData.health < PlayerData.max_health:
|
||||||
|
var health: float = min(PlayerData.health + PlayerData.health_regen * delta, PlayerData.max_health)
|
||||||
|
PlayerData.set_health(health)
|
||||||
|
|
||||||
|
func _regen_entities(delta: float, entities: Dictionary) -> void:
|
||||||
|
for entity in entities:
|
||||||
if not is_instance_valid(entity):
|
if not is_instance_valid(entity):
|
||||||
continue
|
continue
|
||||||
var data: Dictionary = Stats.entities[entity]
|
var data: Dictionary = entities[entity]
|
||||||
if not data["alive"]:
|
if not data["alive"]:
|
||||||
continue
|
continue
|
||||||
var regen: float = data["health_regen"]
|
var regen: float = data["health_regen"]
|
||||||
@@ -16,34 +27,6 @@ func _process(delta: float) -> void:
|
|||||||
data["health"] = min(data["health"] + regen * delta, data["max_health"])
|
data["health"] = min(data["health"] + regen * delta, data["max_health"])
|
||||||
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
|
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
|
||||||
|
|
||||||
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
|
func _emit_initial() -> void:
|
||||||
if not Stats.is_alive(target):
|
EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health)
|
||||||
return
|
EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield)
|
||||||
var remaining: float = amount
|
|
||||||
var shield_system: Node = get_node_or_null("../ShieldSystem")
|
|
||||||
if shield_system:
|
|
||||||
remaining = shield_system.absorb(target, remaining)
|
|
||||||
EventBus.damage_dealt.emit(attacker, target, amount)
|
|
||||||
if remaining > 0:
|
|
||||||
_take_damage(target, remaining)
|
|
||||||
|
|
||||||
func _take_damage(entity: Node, amount: float) -> void:
|
|
||||||
var health: float = Stats.get_stat(entity, "health")
|
|
||||||
health -= amount
|
|
||||||
if health <= 0:
|
|
||||||
health = 0
|
|
||||||
Stats.set_stat(entity, "health", health)
|
|
||||||
var max_health: float = Stats.get_stat(entity, "max_health")
|
|
||||||
EventBus.health_changed.emit(entity, health, max_health)
|
|
||||||
if health <= 0:
|
|
||||||
Stats.set_stat(entity, "alive", false)
|
|
||||||
EventBus.entity_died.emit(entity)
|
|
||||||
|
|
||||||
func _on_heal_requested(healer: Node, target: Node, amount: float) -> void:
|
|
||||||
if not Stats.is_alive(target):
|
|
||||||
return
|
|
||||||
var health: float = Stats.get_stat(target, "health")
|
|
||||||
var max_health: float = Stats.get_stat(target, "max_health")
|
|
||||||
health = min(health + amount, max_health)
|
|
||||||
Stats.set_stat(target, "health", health)
|
|
||||||
EventBus.health_changed.emit(target, health, max_health)
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://b3wkn5118dimy
|
uid://h362ftxb0cns
|
||||||
|
|||||||
@@ -1,75 +1,105 @@
|
|||||||
extends CanvasLayer
|
extends Node
|
||||||
|
|
||||||
const GCD_TIME := 0.5
|
const GCD_TIME := 0.5
|
||||||
|
const ICON_SIZE := 20
|
||||||
@onready var health_bar: ProgressBar = $HealthBar
|
const FONT_SIZE := 14
|
||||||
@onready var health_label: Label = $HealthBar/HealthLabel
|
const BORDER_WIDTH := 2
|
||||||
@onready var shield_bar: ProgressBar = $ShieldBar
|
const MARGIN := 2
|
||||||
@onready var shield_label: Label = $ShieldBar/ShieldLabel
|
|
||||||
@onready var respawn_label: Label = $RespawnTimer
|
|
||||||
@onready var class_icon: Label = $AbilityBar/ClassIcon/Label
|
|
||||||
@onready var ability_panels: Array = [
|
|
||||||
$AbilityBar/Ability1,
|
|
||||||
$AbilityBar/Ability2,
|
|
||||||
$AbilityBar/Ability3,
|
|
||||||
$AbilityBar/Ability4,
|
|
||||||
$AbilityBar/Ability5,
|
|
||||||
]
|
|
||||||
|
|
||||||
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
|
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
|
||||||
var effect_container: HBoxContainer = null
|
var effect_container: HBoxContainer = null
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
respawn_label.visible = false
|
|
||||||
_create_effect_container()
|
|
||||||
EventBus.health_changed.connect(_on_health_changed)
|
EventBus.health_changed.connect(_on_health_changed)
|
||||||
EventBus.shield_changed.connect(_on_shield_changed)
|
EventBus.shield_changed.connect(_on_shield_changed)
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
EventBus.entity_died.connect(_on_entity_died)
|
||||||
EventBus.player_respawned.connect(_on_player_respawned)
|
EventBus.player_respawned.connect(_on_player_respawned)
|
||||||
EventBus.role_changed.connect(_on_role_changed)
|
|
||||||
EventBus.respawn_tick.connect(_on_respawn_tick)
|
EventBus.respawn_tick.connect(_on_respawn_tick)
|
||||||
|
EventBus.role_changed.connect(_on_role_changed)
|
||||||
EventBus.cooldown_tick.connect(_on_cooldown_tick)
|
EventBus.cooldown_tick.connect(_on_cooldown_tick)
|
||||||
EventBus.effect_applied.connect(_on_effect_applied)
|
EventBus.effect_applied.connect(_on_effect_applied)
|
||||||
EventBus.effect_expired.connect(_on_effect_expired)
|
EventBus.effect_expired.connect(_on_effect_expired)
|
||||||
|
_init_hud.call_deferred()
|
||||||
|
|
||||||
|
func _init_hud() -> void:
|
||||||
|
var hud: CanvasLayer = _get_hud()
|
||||||
|
if not hud:
|
||||||
|
return
|
||||||
|
hud.get_node("RespawnTimer").visible = false
|
||||||
|
effect_container = HBoxContainer.new()
|
||||||
|
effect_container.name = "EffectContainer"
|
||||||
|
effect_container.position = Vector2(10, 60)
|
||||||
|
effect_container.add_theme_constant_override("separation", 3)
|
||||||
|
hud.add_child(effect_container)
|
||||||
|
|
||||||
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
|
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
|
||||||
if entity.name == "Player":
|
if entity != PlayerData:
|
||||||
health_bar.max_value = max_val
|
return
|
||||||
health_bar.value = current
|
var hud: CanvasLayer = _get_hud()
|
||||||
health_label.text = "%d/%d" % [current, max_val]
|
if not hud:
|
||||||
|
return
|
||||||
|
var bar: ProgressBar = hud.get_node("HealthBar")
|
||||||
|
bar.max_value = max_val
|
||||||
|
bar.value = current
|
||||||
|
hud.get_node("HealthBar/HealthLabel").text = "%d/%d" % [current, max_val]
|
||||||
|
|
||||||
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
|
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
|
||||||
if entity.name == "Player":
|
if entity != PlayerData:
|
||||||
shield_bar.max_value = max_val
|
return
|
||||||
shield_bar.value = current
|
var hud: CanvasLayer = _get_hud()
|
||||||
shield_label.text = "%d/%d" % [current, max_val]
|
if not hud:
|
||||||
|
return
|
||||||
|
var bar: ProgressBar = hud.get_node("ShieldBar")
|
||||||
|
bar.max_value = max_val
|
||||||
|
bar.value = current
|
||||||
|
hud.get_node("ShieldBar/ShieldLabel").text = "%d/%d" % [current, max_val]
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
func _on_entity_died(entity: Node) -> void:
|
||||||
if entity.name == "Player":
|
if entity != PlayerData:
|
||||||
respawn_label.visible = true
|
return
|
||||||
|
var hud: CanvasLayer = _get_hud()
|
||||||
|
if hud:
|
||||||
|
hud.get_node("RespawnTimer").visible = true
|
||||||
|
|
||||||
func _on_player_respawned(_player: Node) -> void:
|
func _on_player_respawned(_player: Node) -> void:
|
||||||
respawn_label.visible = false
|
var hud: CanvasLayer = _get_hud()
|
||||||
|
if hud:
|
||||||
|
hud.get_node("RespawnTimer").visible = false
|
||||||
|
|
||||||
func _on_respawn_tick(timer: float) -> void:
|
func _on_respawn_tick(timer: float) -> void:
|
||||||
respawn_label.text = str(ceil(timer))
|
var hud: CanvasLayer = _get_hud()
|
||||||
|
if hud:
|
||||||
|
hud.get_node("RespawnTimer").text = str(ceil(timer))
|
||||||
|
|
||||||
func _on_role_changed(_player: Node, role_type: int) -> void:
|
func _on_role_changed(_player: Node, role_type: int) -> void:
|
||||||
|
var hud: CanvasLayer = _get_hud()
|
||||||
|
if not hud:
|
||||||
|
return
|
||||||
|
var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label")
|
||||||
match role_type:
|
match role_type:
|
||||||
0: class_icon.text = "T"
|
0: icon.text = "T"
|
||||||
1: class_icon.text = "D"
|
1: icon.text = "D"
|
||||||
2: class_icon.text = "H"
|
2: icon.text = "H"
|
||||||
|
|
||||||
func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void:
|
func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void:
|
||||||
for i in range(min(ability_panels.size(), cooldowns.size())):
|
var hud: CanvasLayer = _get_hud()
|
||||||
var panel: Panel = ability_panels[i]
|
if not hud:
|
||||||
|
return
|
||||||
|
var panels: Array = [
|
||||||
|
hud.get_node("AbilityBar/Ability1"),
|
||||||
|
hud.get_node("AbilityBar/Ability2"),
|
||||||
|
hud.get_node("AbilityBar/Ability3"),
|
||||||
|
hud.get_node("AbilityBar/Ability4"),
|
||||||
|
hud.get_node("AbilityBar/Ability5"),
|
||||||
|
]
|
||||||
|
for i in range(min(panels.size(), cooldowns.size())):
|
||||||
|
var panel: Panel = panels[i]
|
||||||
var label: Label = panel.get_node("Label")
|
var label: Label = panel.get_node("Label")
|
||||||
var overlay: ColorRect = panel.get_node("CooldownOverlay")
|
var overlay: ColorRect = panel.get_node("CooldownOverlay")
|
||||||
var cd: float = cooldowns[i]
|
var cd: float = cooldowns[i]
|
||||||
var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0
|
var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0
|
||||||
var active_cd: float = max(cd, gcd)
|
var active_cd: float = max(cd, gcd)
|
||||||
var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME
|
var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME
|
||||||
|
|
||||||
if active_cd > 0:
|
if active_cd > 0:
|
||||||
var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0)
|
var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0)
|
||||||
overlay.visible = true
|
overlay.visible = true
|
||||||
@@ -79,43 +109,19 @@ func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float)
|
|||||||
overlay.visible = false
|
overlay.visible = false
|
||||||
label.text = ability_labels[i]
|
label.text = ability_labels[i]
|
||||||
|
|
||||||
func _create_effect_container() -> void:
|
|
||||||
effect_container = HBoxContainer.new()
|
|
||||||
effect_container.name = "EffectContainer"
|
|
||||||
effect_container.position = Vector2(10, 60)
|
|
||||||
effect_container.add_theme_constant_override("separation", 3)
|
|
||||||
add_child(effect_container)
|
|
||||||
|
|
||||||
func _on_effect_applied(target: Node, effect: Effect) -> void:
|
func _on_effect_applied(target: Node, effect: Effect) -> void:
|
||||||
if target.name != "Player":
|
if target != PlayerData:
|
||||||
return
|
return
|
||||||
var panel := _create_icon_panel(effect)
|
if effect_container:
|
||||||
var insert_idx: int = _get_sorted_index(effect.type)
|
_add_icon(effect)
|
||||||
effect_container.add_child(panel)
|
|
||||||
effect_container.move_child(panel, insert_idx)
|
|
||||||
|
|
||||||
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
||||||
if target.name != "Player":
|
if target != PlayerData:
|
||||||
return
|
return
|
||||||
for child in effect_container.get_children():
|
if effect_container:
|
||||||
if child.has_meta("effect_type") and child.has_meta("effect_name"):
|
_remove_icon(effect)
|
||||||
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
|
|
||||||
child.queue_free()
|
|
||||||
return
|
|
||||||
|
|
||||||
func _get_sorted_index(type: int) -> int:
|
func _add_icon(effect: Effect) -> void:
|
||||||
var idx := 0
|
|
||||||
for child in effect_container.get_children():
|
|
||||||
if not child.has_meta("effect_type"):
|
|
||||||
continue
|
|
||||||
var child_type: int = child.get_meta("effect_type")
|
|
||||||
if child_type <= type:
|
|
||||||
idx += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return idx
|
|
||||||
|
|
||||||
func _create_icon_panel(effect: Effect) -> PanelContainer:
|
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
var style := StyleBoxFlat.new()
|
var style := StyleBoxFlat.new()
|
||||||
match effect.type:
|
match effect.type:
|
||||||
@@ -128,18 +134,35 @@ func _create_icon_panel(effect: Effect) -> PanelContainer:
|
|||||||
Effect.Type.DEBUFF:
|
Effect.Type.DEBUFF:
|
||||||
style.bg_color = Color(0.3, 0.15, 0.15, 1)
|
style.bg_color = Color(0.3, 0.15, 0.15, 1)
|
||||||
style.border_color = Color(1.0, 0.3, 0.3, 1)
|
style.border_color = Color(1.0, 0.3, 0.3, 1)
|
||||||
style.set_border_width_all(2)
|
style.set_border_width_all(BORDER_WIDTH)
|
||||||
style.set_content_margin_all(2)
|
style.set_content_margin_all(MARGIN)
|
||||||
panel.add_theme_stylebox_override("panel", style)
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.text = effect.effect_name.left(1)
|
label.text = effect.effect_name.left(1)
|
||||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
label.add_theme_font_size_override("font_size", 14)
|
label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||||
label.add_theme_color_override("font_color", Color.WHITE)
|
label.add_theme_color_override("font_color", Color.WHITE)
|
||||||
label.custom_minimum_size = Vector2(20, 20)
|
label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
|
||||||
panel.add_child(label)
|
panel.add_child(label)
|
||||||
panel.custom_minimum_size = Vector2(24, 24)
|
panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2)
|
||||||
panel.set_meta("effect_type", effect.type)
|
panel.set_meta("effect_type", effect.type)
|
||||||
panel.set_meta("effect_name", effect.effect_name)
|
panel.set_meta("effect_name", effect.effect_name)
|
||||||
return panel
|
var insert_idx := 0
|
||||||
|
for child in effect_container.get_children():
|
||||||
|
if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type:
|
||||||
|
insert_idx += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
effect_container.add_child(panel)
|
||||||
|
effect_container.move_child(panel, insert_idx)
|
||||||
|
|
||||||
|
func _remove_icon(effect: Effect) -> void:
|
||||||
|
for child in effect_container.get_children():
|
||||||
|
if child.has_meta("effect_type") and child.has_meta("effect_name"):
|
||||||
|
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
|
||||||
|
child.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
func _get_hud() -> CanvasLayer:
|
||||||
|
return get_tree().get_first_node_in_group("hud") as CanvasLayer
|
||||||
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
|
extends Node
|
||||||
|
|
||||||
var dead_players: Dictionary = {}
|
var respawn_timer := 0.0
|
||||||
|
var is_dead := false
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
EventBus.entity_died.connect(_on_entity_died)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
for player in dead_players.keys():
|
if not is_dead:
|
||||||
if not is_instance_valid(player):
|
return
|
||||||
dead_players.erase(player)
|
respawn_timer -= delta
|
||||||
continue
|
EventBus.respawn_tick.emit(respawn_timer)
|
||||||
dead_players[player] -= delta
|
if respawn_timer <= 0:
|
||||||
EventBus.respawn_tick.emit(dead_players[player])
|
_respawn()
|
||||||
if dead_players[player] <= 0:
|
|
||||||
_respawn(player)
|
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
func _on_entity_died(entity: Node) -> void:
|
||||||
if not entity.is_in_group("player"):
|
if not entity.is_in_group("player"):
|
||||||
return
|
return
|
||||||
if entity in dead_players:
|
if is_dead:
|
||||||
return
|
return
|
||||||
var base: BaseStats = Stats.get_base(entity)
|
is_dead = true
|
||||||
var respawn_time: float = base.respawn_time if base is PlayerStats else 3.0
|
respawn_timer = PlayerData.respawn_time
|
||||||
dead_players[entity] = respawn_time
|
|
||||||
entity.velocity = Vector3.ZERO
|
entity.velocity = Vector3.ZERO
|
||||||
entity.get_node("Mesh").visible = false
|
entity.get_node("Mesh").visible = false
|
||||||
entity.get_node("CollisionShape3D").disabled = true
|
entity.get_node("CollisionShape3D").disabled = true
|
||||||
entity.get_node("Movement").set_physics_process(false)
|
entity.get_node("Movement").set_physics_process(false)
|
||||||
entity.get_node("Combat").set_process_unhandled_input(false)
|
entity.get_node("Ability").set_process_unhandled_input(false)
|
||||||
entity.get_node("Targeting").set_process_unhandled_input(false)
|
entity.get_node("Targeting").set_process_unhandled_input(false)
|
||||||
|
|
||||||
func _respawn(player: Node) -> void:
|
func _respawn() -> void:
|
||||||
dead_players.erase(player)
|
is_dead = false
|
||||||
|
var player: Node = get_tree().get_first_node_in_group("player")
|
||||||
|
if not player:
|
||||||
|
return
|
||||||
player.global_position = Vector3(0, 1, -5)
|
player.global_position = Vector3(0, 1, -5)
|
||||||
player.get_node("Mesh").visible = true
|
player.get_node("Mesh").visible = true
|
||||||
player.get_node("CollisionShape3D").disabled = false
|
player.get_node("CollisionShape3D").disabled = false
|
||||||
player.get_node("Movement").set_physics_process(true)
|
player.get_node("Movement").set_physics_process(true)
|
||||||
player.get_node("Combat").set_process_unhandled_input(true)
|
player.get_node("Ability").set_process_unhandled_input(true)
|
||||||
player.get_node("Targeting").set_process_unhandled_input(true)
|
player.get_node("Targeting").set_process_unhandled_input(true)
|
||||||
var max_health: float = Stats.get_stat(player, "max_health")
|
PlayerData.respawn()
|
||||||
var max_shield: float = Stats.get_stat(player, "max_shield")
|
|
||||||
Stats.set_stat(player, "health", max_health)
|
|
||||||
Stats.set_stat(player, "shield", max_shield)
|
|
||||||
Stats.set_stat(player, "alive", true)
|
|
||||||
EventBus.health_changed.emit(player, max_health, max_health)
|
|
||||||
EventBus.shield_changed.emit(player, max_shield, max_shield)
|
|
||||||
EventBus.player_respawned.emit(player)
|
|
||||||
|
|||||||
23
systems/role_system.gd
Normal file
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