From f1d34ebf1dac1113ca2d14f2455e6b82748efb5c Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Sat, 4 Apr 2026 00:00:15 +0200 Subject: [PATCH] update --- CLAUDE.md | 14 +- autoloads/boss_stats.gd | 64 +++++ autoloads/boss_stats.gd.uid | 1 + autoloads/enemy_stats.gd | 64 +++++ autoloads/enemy_stats.gd.uid | 1 + autoloads/event_bus.gd | 7 +- autoloads/game_state.gd | 20 -- autoloads/game_state.gd.uid | 1 - autoloads/player_stats.gd | 145 +++++++++++ autoloads/player_stats.gd.uid | 1 + autoloads/portal_stats.gd | 45 ++++ autoloads/portal_stats.gd.uid | 1 + autoloads/stats/stats.gd | 53 ----- autoloads/stats/stats.gd.uid | 1 - comminication.md | 26 ++ features.md | 63 +++++ infosammlung/Level 1.md | 46 ---- infosammlung/Level 2.md | 60 ----- infosammlung/Level 3.md | 110 --------- infosammlung/Planung.md | 26 -- infosammlung/Szenarien.md | 38 --- infosammlung/idden.md | 70 ------ infosammlung/story.md | 58 ----- plan.md | 58 +++-- plan2.md | 72 ++++++ project.godot | 6 +- scenes/dungeon/dungeon.tscn | 69 ++++-- scenes/dungeon/dungeon_manager.gd.uid | 1 - scenes/enemy/boss.gd | 5 - scenes/enemy/boss.gd.uid | 1 - scenes/enemy/boss.tscn | 108 --------- scenes/enemy/boss_stats.gd.uid | 2 +- scenes/enemy/detection.gd | 11 + scenes/enemy/detection.gd.uid | 1 + scenes/enemy/enemy.gd | 37 --- scenes/enemy/enemy.gd.uid | 1 - scenes/enemy/enemy.tscn | 68 +++--- scenes/enemy/enemy_movement.gd | 62 ----- scenes/enemy/enemy_movement.gd.uid | 1 - scenes/enemy/init.gd | 30 +++ scenes/enemy/init.gd.uid | 1 + scenes/healthbar.gd | 159 ------------- scenes/healthbar.gd.uid | 1 - scenes/hud/hud.gd.uid | 1 - scenes/hud/hud.tscn | 5 +- scenes/player/{combat.gd => ability.gd} | 2 +- scenes/player/ability.gd.uid | 1 + scenes/player/combat.gd.uid | 1 - scenes/player/init.gd | 12 + scenes/player/init.gd.uid | 1 + scenes/player/movement.gd | 14 +- scenes/player/player.gd | 28 --- scenes/player/player.gd.uid | 1 - scenes/player/player.tscn | 32 +-- scenes/player/role/role.gd | 34 +-- scenes/player/targeting.gd | 83 +------ scenes/portal/gate.gd | 8 +- scenes/portal/init.gd | 14 ++ scenes/portal/init.gd.uid | 1 + scenes/portal/portal.gd | 33 --- scenes/portal/portal.gd.uid | 1 - scenes/portal/portal.tscn | 4 +- scenes/world/portal_spawner.gd | 8 +- scenes/world/world.tscn | 134 +++++++---- systems/ability_system.gd | 142 ++++------- systems/aggro/aggro_decay.gd | 15 +- systems/aggro/aggro_events.gd | 15 +- systems/aggro/aggro_tracker.gd | 42 ++-- systems/ai_system.gd | 91 +++++++ systems/ai_system.gd.uid | 1 + systems/attack_system.gd | 27 +++ systems/attack_system.gd.uid | 1 + systems/aura_system.gd | 62 +++++ systems/aura_system.gd.uid | 1 + systems/buff_system.gd | 142 +++++++++++ systems/buff_system.gd.uid | 1 + systems/cooldown_system.gd | 78 +----- systems/damage_system.gd | 37 +++ systems/damage_system.gd.uid | 2 +- systems/debuff_system.gd | 65 +++++ systems/debuff_system.gd.uid | 1 + .../dungeon_system.gd | 9 +- systems/dungeon_system.gd.uid | 1 + systems/effect_system.gd | 190 --------------- systems/effect_system.gd.uid | 1 - systems/enemy_ai_system.gd | 36 --- systems/enemy_ai_system.gd.uid | 1 - systems/heal_system.gd | 22 ++ systems/heal_system.gd.uid | 1 + systems/health_system.gd | 53 ++--- systems/health_system.gd.uid | 2 +- scenes/hud/hud.gd => systems/hud_system.gd | 169 +++++++------ systems/hud_system.gd.uid | 1 + systems/nameplate_system.gd | 225 ++++++++++++++++++ systems/nameplate_system.gd.uid | 1 + systems/portal_system.gd | 21 ++ systems/portal_system.gd.uid | 1 + systems/respawn_system.gd | 44 ++-- systems/role_system.gd | 23 ++ systems/role_system.gd.uid | 1 + systems/shield_system.gd | 54 ++++- systems/spawn_system.gd | 28 +-- systems/targeting_system.gd | 66 +++++ systems/targeting_system.gd.uid | 1 + 104 files changed, 1912 insertions(+), 1789 deletions(-) create mode 100644 autoloads/boss_stats.gd create mode 100644 autoloads/boss_stats.gd.uid create mode 100644 autoloads/enemy_stats.gd create mode 100644 autoloads/enemy_stats.gd.uid delete mode 100644 autoloads/game_state.gd delete mode 100644 autoloads/game_state.gd.uid create mode 100644 autoloads/player_stats.gd create mode 100644 autoloads/player_stats.gd.uid create mode 100644 autoloads/portal_stats.gd create mode 100644 autoloads/portal_stats.gd.uid delete mode 100644 autoloads/stats/stats.gd delete mode 100644 autoloads/stats/stats.gd.uid create mode 100644 comminication.md create mode 100644 features.md delete mode 100644 infosammlung/Level 1.md delete mode 100644 infosammlung/Level 2.md delete mode 100644 infosammlung/Level 3.md delete mode 100644 infosammlung/Planung.md delete mode 100644 infosammlung/Szenarien.md delete mode 100644 infosammlung/idden.md delete mode 100644 infosammlung/story.md create mode 100644 plan2.md delete mode 100644 scenes/dungeon/dungeon_manager.gd.uid delete mode 100644 scenes/enemy/boss.gd delete mode 100644 scenes/enemy/boss.gd.uid delete mode 100644 scenes/enemy/boss.tscn create mode 100644 scenes/enemy/detection.gd create mode 100644 scenes/enemy/detection.gd.uid delete mode 100644 scenes/enemy/enemy.gd delete mode 100644 scenes/enemy/enemy.gd.uid delete mode 100644 scenes/enemy/enemy_movement.gd delete mode 100644 scenes/enemy/enemy_movement.gd.uid create mode 100644 scenes/enemy/init.gd create mode 100644 scenes/enemy/init.gd.uid delete mode 100644 scenes/healthbar.gd delete mode 100644 scenes/healthbar.gd.uid delete mode 100644 scenes/hud/hud.gd.uid rename scenes/player/{combat.gd => ability.gd} (79%) create mode 100644 scenes/player/ability.gd.uid delete mode 100644 scenes/player/combat.gd.uid create mode 100644 scenes/player/init.gd create mode 100644 scenes/player/init.gd.uid delete mode 100644 scenes/player/player.gd delete mode 100644 scenes/player/player.gd.uid create mode 100644 scenes/portal/init.gd create mode 100644 scenes/portal/init.gd.uid delete mode 100644 scenes/portal/portal.gd delete mode 100644 scenes/portal/portal.gd.uid create mode 100644 systems/ai_system.gd create mode 100644 systems/ai_system.gd.uid create mode 100644 systems/attack_system.gd create mode 100644 systems/attack_system.gd.uid create mode 100644 systems/aura_system.gd create mode 100644 systems/aura_system.gd.uid create mode 100644 systems/buff_system.gd create mode 100644 systems/buff_system.gd.uid create mode 100644 systems/debuff_system.gd create mode 100644 systems/debuff_system.gd.uid rename scenes/dungeon/dungeon_manager.gd => systems/dungeon_system.gd (58%) create mode 100644 systems/dungeon_system.gd.uid delete mode 100644 systems/effect_system.gd delete mode 100644 systems/effect_system.gd.uid delete mode 100644 systems/enemy_ai_system.gd delete mode 100644 systems/enemy_ai_system.gd.uid create mode 100644 systems/heal_system.gd create mode 100644 systems/heal_system.gd.uid rename scenes/hud/hud.gd => systems/hud_system.gd (57%) create mode 100644 systems/hud_system.gd.uid create mode 100644 systems/nameplate_system.gd create mode 100644 systems/nameplate_system.gd.uid create mode 100644 systems/portal_system.gd create mode 100644 systems/portal_system.gd.uid create mode 100644 systems/role_system.gd create mode 100644 systems/role_system.gd.uid create mode 100644 systems/targeting_system.gd create mode 100644 systems/targeting_system.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index 28bebc0..db0d8ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,14 +27,15 @@ Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare n Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. - `scenes/` — Darstellung + Input - `player/` — Spieler + player_stats + role/ (Rollen + Abilities) - - `enemy/` — Gegner + Boss + enemy_stats + boss_stats + - `enemy/` — Gegner + enemy_stats + boss/ (Boss + boss_stats) - `portal/` — Portal + Gate + portal_stats - `dungeon/` — Dungeon + dungeon_manager - `hud/` — HUD - `world/` — Hauptszene + portal_spawner - - `healthbar.gd` — Shared Component + - `effect_icon_factory.gd` — Shared Utility (Effekt-Icons) + - `healthbar*.gd` — Healthbar-Komponenten (health, shield, status, effects) - `systems/` — Spiellogik - - 11 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element) + - 12 Systeme (health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, aura, buff_calc) - `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten) - `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config - `autoloads/` — Globaler Zustand @@ -44,13 +45,6 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. ## Planungsdokument `plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand. -## Design-Dokumente -Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design-Docs: -- `story.md` — Gameplay-Loop, Szenarien, Steuerung -- `idden.md` — Alle Ideen, Kernphilosophie, Technik -- `Szenarien.md` — Ressourcenanfragen, Dungeons, Gemeinschaft, Endgame -- `Level 1.md` bis `Level 3.md` — Systeme nach Priorität - ## Core Loop 1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum) 2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%) diff --git a/autoloads/boss_stats.gd b/autoloads/boss_stats.gd new file mode 100644 index 0000000..9533018 --- /dev/null +++ b/autoloads/boss_stats.gd @@ -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) diff --git a/autoloads/boss_stats.gd.uid b/autoloads/boss_stats.gd.uid new file mode 100644 index 0000000..4e97697 --- /dev/null +++ b/autoloads/boss_stats.gd.uid @@ -0,0 +1 @@ +uid://dbr02t7pt4vcn diff --git a/autoloads/enemy_stats.gd b/autoloads/enemy_stats.gd new file mode 100644 index 0000000..9533018 --- /dev/null +++ b/autoloads/enemy_stats.gd @@ -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) diff --git a/autoloads/enemy_stats.gd.uid b/autoloads/enemy_stats.gd.uid new file mode 100644 index 0000000..6ab7faf --- /dev/null +++ b/autoloads/enemy_stats.gd.uid @@ -0,0 +1 @@ +uid://bvxn6y15tvidu diff --git a/autoloads/event_bus.gd b/autoloads/event_bus.gd index 23d6698..a9a6d8d 100644 --- a/autoloads/event_bus.gd +++ b/autoloads/event_bus.gd @@ -1,8 +1,12 @@ extends Node # Intentionen (Input → System) -signal ability_use_requested(player, ability_index) +signal ability_use(player, ability_index) +signal role_change_requested(player, role) +signal target_requested(player, target) signal enemy_detected(enemy, player) +signal enemy_lost(enemy, player) +signal portal_entered(portal, player) # Kampf signal attack_executed(attacker, position, direction, damage) @@ -29,7 +33,6 @@ signal buff_changed(entity, stat, value) # Gegner signal enemy_engaged(enemy, target) -signal enemy_lost(enemy, player) # Portal signal portal_spawn(portal, enemies) diff --git a/autoloads/game_state.gd b/autoloads/game_state.gd deleted file mode 100644 index dcaae09..0000000 --- a/autoloads/game_state.gd +++ /dev/null @@ -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 diff --git a/autoloads/game_state.gd.uid b/autoloads/game_state.gd.uid deleted file mode 100644 index fee2af4..0000000 --- a/autoloads/game_state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cp2vadwcd12sm diff --git a/autoloads/player_stats.gd b/autoloads/player_stats.gd new file mode 100644 index 0000000..2310063 --- /dev/null +++ b/autoloads/player_stats.gd @@ -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() diff --git a/autoloads/player_stats.gd.uid b/autoloads/player_stats.gd.uid new file mode 100644 index 0000000..9417cb1 --- /dev/null +++ b/autoloads/player_stats.gd.uid @@ -0,0 +1 @@ +uid://blmuqkl3aro5w diff --git a/autoloads/portal_stats.gd b/autoloads/portal_stats.gd new file mode 100644 index 0000000..8e67f41 --- /dev/null +++ b/autoloads/portal_stats.gd @@ -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) diff --git a/autoloads/portal_stats.gd.uid b/autoloads/portal_stats.gd.uid new file mode 100644 index 0000000..6356dbf --- /dev/null +++ b/autoloads/portal_stats.gd.uid @@ -0,0 +1 @@ +uid://doullpjapcsk1 diff --git a/autoloads/stats/stats.gd b/autoloads/stats/stats.gd deleted file mode 100644 index 532f9aa..0000000 --- a/autoloads/stats/stats.gd +++ /dev/null @@ -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 diff --git a/autoloads/stats/stats.gd.uid b/autoloads/stats/stats.gd.uid deleted file mode 100644 index fd713ad..0000000 --- a/autoloads/stats/stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cyxmpeib7pcw7 diff --git a/comminication.md b/comminication.md new file mode 100644 index 0000000..0901851 --- /dev/null +++ b/comminication.md @@ -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) diff --git a/features.md b/features.md new file mode 100644 index 0000000..18e23d7 --- /dev/null +++ b/features.md @@ -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 diff --git a/infosammlung/Level 1.md b/infosammlung/Level 1.md deleted file mode 100644 index 6715ad4..0000000 --- a/infosammlung/Level 1.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/infosammlung/Level 2.md b/infosammlung/Level 2.md deleted file mode 100644 index 77ca549..0000000 --- a/infosammlung/Level 2.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/infosammlung/Level 3.md b/infosammlung/Level 3.md deleted file mode 100644 index ac9bf2c..0000000 --- a/infosammlung/Level 3.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/infosammlung/Planung.md b/infosammlung/Planung.md deleted file mode 100644 index 53d3b28..0000000 --- a/infosammlung/Planung.md +++ /dev/null @@ -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 diff --git a/infosammlung/Szenarien.md b/infosammlung/Szenarien.md deleted file mode 100644 index 6188883..0000000 --- a/infosammlung/Szenarien.md +++ /dev/null @@ -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 --> ... \ No newline at end of file diff --git a/infosammlung/idden.md b/infosammlung/idden.md deleted file mode 100644 index 3b315af..0000000 --- a/infosammlung/idden.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/infosammlung/story.md b/infosammlung/story.md deleted file mode 100644 index bc92380..0000000 --- a/infosammlung/story.md +++ /dev/null @@ -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 - diff --git a/plan.md b/plan.md index 8891933..95e727e 100644 --- a/plan.md +++ b/plan.md @@ -4,21 +4,27 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. ``` scenes/ — Darstellung + Input - healthbar.gd — Shared Component + effect_icon_factory.gd — Shared Utility (statisch, Effekt-Icon-Erstellung) + healthbar.gd — Health-Anzeige + Viewport-Setup + healthbar_shield.gd — Shield-Anzeige + healthbar_status.gd — Target-Border + Aggro-Farbwechsel + healthbar_effects.gd — Effekt-Icons auf Healthbar player/ — Spieler + player_stats role/ — Rollenwechsel + Ability/AbilitySet-Klassen damage/ — set.tres + abilities/ tank/ — set.tres + abilities/ healer/ — set.tres + abilities/ - enemy/ — Gegner + Boss + enemy_stats + boss_stats + enemy/ — Gegner + enemy_stats + boss/ — Boss + boss_stats portal/ — Portal + Gate + portal_stats dungeon/ — Dungeon + dungeon_manager - hud/ — HUD + hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects) world/ — Hauptszene + portal_spawner systems/ — Spiellogik aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config effect.gd — Effect Resource (Buff/Debuff/Aura Daten) - 10× *_system.gd — health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element + 11× *_system.gd — health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, buff_calc + aura_system.gd — Aura-Propagierung (Child von EffectSystem) autoloads/ — Globaler Zustand event_bus.gd game_state.gd @@ -27,7 +33,7 @@ autoloads/ — Globaler Zustand ## Szenenbaum - Welt - - Systems (11 Systeme als Child-Nodes) + - Systems (12 Systeme als Child-Nodes) - Taverne - Player - Portale (dynamisch) @@ -136,20 +142,26 @@ autoloads/ — Globaler Zustand - Listener: entity_died - Event: respawn_tick, player_respawned ### AbilitySystem (ability_system.gd) -- Ability-Ausführung (Single, AOE, Utility, Ult) + Auto-Attack in _process +- Ability-Ausführung (Single, AOE, Utility, Ult) - Listener: ability_use_requested - Event: attack_executed, damage_requested, heal_requested +### AutoAttackSystem (auto_attack_system.gd) +- Auto-Attack-Logik, läuft jeden Frame in _process +- Liest CombatState.in_combat + Targeting.current_target +- Event: damage_requested, heal_requested ### EffectSystem (effect_system.gd) - Verwaltet Buffs, Debuffs und Auras auf Entities - Effect Resource (effect.gd): effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration, is_multiplier, aura_radius, tick_interval, element - State: active_effects Dictionary[Node, Array[Dict]] (effect, source, remaining, tick_timer) - Kein Stacking: gleicher effect_name auf Entity → wird refreshed statt gestackt - Passive-Abilities werden als AURA-Effekte erstellt (role_changed → permanente Auras) -- Aura-Propagierung: Auras mit aura_radius > 0 geben allen Spielern im Radius einen temporären Buff (0.5s, refreshed jedes Frame). Verlässt man den Radius → Buff sofort weg -- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks, Aura-Propagierung -- _recalc_stat_buffs: Multiplier-Effekte aggregieren → buff_damage/heal/shield in Stats +- _process: Dauer ticken, abgelaufene entfernen, DoT/HoT-Ticks - Listener: role_changed, entity_died, effect_requested -- Event: buff_changed, shield_changed, effect_applied, effect_expired +- Event: effect_applied, effect_expired +- Children: + - AuraSystem (aura_system.gd) — Aura-Propagierung im Radius, Buff-Refresh + - BuffCalcSystem (buff_calc_system.gd) — Multiplier aggregieren → Stats + Shield updaten + - Event: buff_changed, shield_changed ### ElementSystem (element_system.gd) - Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus - Element Enum: NONE, FIRE (erweiterbar) @@ -164,8 +176,6 @@ autoloads/ — Globaler Zustand - Cooldown-Tracking, GCD, AA-Timer per Entity - register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem - Event: cooldown_tick -### DamageSystem (damage_system.gd) -- Reserviert für spätere Schadensberechnung (aktuell leer) ### AggroSystem (systems/aggro/) - Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres) - aggro_system.gd — Parent, Config halten, Children verdrahten @@ -209,8 +219,9 @@ autoloads/ — Globaler Zustand - Camera3D - Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats - Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested + - CombatState (Node, combat_state.gd) — in_combat-Tracking, Combat-Timer - Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready) - - Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested + - Targeting (Node, targeting.gd) — Klick/TAB/Auto-Target, emittiert target_changed - player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn - camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung @@ -223,7 +234,10 @@ autoloads/ — Globaler Zustand - DetectionArea (Area3D, emittiert enemy_detected) - NavigationAgent3D - EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle - - Healthbar (Sprite3D + SubViewport, healthbar.gd) — liest HP/Shield von Stats, zeigt Effekt-Icons + - Healthbar (Sprite3D + SubViewport, healthbar.gd) — Health-Anzeige + - HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige + - HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe + - HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons - enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal - Aggro-Regeln (Werte in AggroConfig Resource): - Aufbau: @@ -254,6 +268,9 @@ autoloads/ — Globaler Zustand - HitArea (Area3D) - DetectionArea (Area3D, Auto-Targeting bei Betreten) - Healthbar (Sprite3D + SubViewport, healthbar.gd) + - HealthbarShield (Node, healthbar_shield.gd) + - HealthbarStatus (Node, healthbar_status.gd) + - HealthbarEffects (Node, healthbar_effects.gd) - portal.gd — Registriert bei Stats mit PortalStats Resource - Spawnt Gegner bei HP-Schwellen (→ SpawnSystem) @@ -266,7 +283,7 @@ autoloads/ — Globaler Zustand ## Dungeon (dungeon/) - dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht) - - Systems (alle 11 Systeme, temporär bis Welt parallel läuft) + - Systems (alle 12 Systeme, temporär bis Welt parallel läuft) - NavigationRegion3D - Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch) - Spieler (Instanz von player.tscn) @@ -278,13 +295,15 @@ autoloads/ — Globaler Zustand - Eigene Systems bis Welt parallel läuft (geplant: Reparenting) ## HUD (hud/) -- hud.tscn — CanvasLayer +- hud.tscn — CanvasLayer (kein Root-Skript) - HealthBar (ProgressBar, Label) - ShieldBar (ProgressBar, Label) - - EffectContainer (HBoxContainer, programmatisch, unter ShieldBar) - RespawnTimer (Label, Countdown bei Tod) - AbilityBar (HBoxContainer, RoleIcon + Abilities 1-4 + Passive) -- hud.gd — Reagiert auf Events, liest Werte von Stats, zeigt Effekt-Icons + - HudVitals (Node, hud_vitals.gd) — HP/Shield-Bars + - HudRespawn (Node, hud_respawn.gd) — Respawn-Timer + - HudAbilities (Node, hud_abilities.gd) — Ability-Bar + Cooldowns + Rollen-Icon + - HudEffects (Node, hud_effects.gd) — Effekt-Icons (nutzt EffectIconFactory) # Abilities (Werte) - Schadens-Klasse: @@ -308,3 +327,6 @@ autoloads/ — Globaler Zustand - 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD - 4 Ult: 25 Heal Single + 10 AOE Heal 3m radius, 20m Range, 15s CD, GCD - 5 Passive: 50% mehr Heal Aura, 50m (permanent aktiv, kein CD) + +--- + diff --git a/plan2.md b/plan2.md new file mode 100644 index 0000000..4640c7a --- /dev/null +++ b/plan2.md @@ -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 | diff --git a/project.godot b/project.godot index c8f1160..88490ae 100644 --- a/project.godot +++ b/project.godot @@ -18,8 +18,10 @@ config/icon="res://icon.svg" [autoload] EventBus="*res://autoloads/event_bus.gd" -Stats="*res://autoloads/stats/stats.gd" -GameState="*res://autoloads/game_state.gd" +PlayerData="*res://autoloads/player_stats.gd" +EnemyData="*res://autoloads/enemy_stats.gd" +BossData="*res://autoloads/boss_stats.gd" +PortalData="*res://autoloads/portal_stats.gd" [dotnet] diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn index a24e84b..58306d6 100644 --- a/scenes/dungeon/dungeon.tscn +++ b/scenes/dungeon/dungeon.tscn @@ -3,23 +3,34 @@ [ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] [ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"] -[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"] -[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"] +[ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"] +[ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"] [ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"] -[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] -[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] [ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] +[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] +[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"] +[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] +[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"] [ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"] +[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"] [ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"] +[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"] [ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] [ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] [ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] [ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] -[ext_resource type="Script" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"] +[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"] [ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"] [ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] -[ext_resource type="Script" path="res://systems/effect_system.gd" id="effect_system"] +[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"] +[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"] +[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"] [ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"] +[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"] +[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"] +[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"] +[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"] +[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"] [sub_resource type="NavigationMesh" id="NavigationMesh_1"] vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0) @@ -58,18 +69,33 @@ size = Vector3(0.5, 3, 90) [node name="HealthSystem" type="Node" parent="Systems"] script = ExtResource("health_system") +[node name="DamageSystem" type="Node" parent="Systems"] +script = ExtResource("damage_system") + +[node name="HealSystem" type="Node" parent="Systems"] +script = ExtResource("heal_system") + [node name="ShieldSystem" type="Node" parent="Systems"] script = ExtResource("shield_system") -[node name="DamageSystem" type="Node" parent="Systems"] -script = ExtResource("damage_system") +[node name="RoleSystem" type="Node" parent="Systems"] +script = ExtResource("role_system") +tank_set = ExtResource("tank_set") +damage_set = ExtResource("damage_set") +healer_set = ExtResource("healer_set") [node name="AbilitySystem" type="Node" parent="Systems"] script = ExtResource("ability_system") +[node name="AttackSystem" type="Node" parent="Systems"] +script = ExtResource("attack_system") + [node name="CooldownSystem" type="Node" parent="Systems"] script = ExtResource("cooldown_system") +[node name="TargetingSystem" type="Node" parent="Systems"] +script = ExtResource("targeting_system") + [node name="AggroSystem" type="Node" parent="Systems"] script = ExtResource("aggro_system") @@ -82,8 +108,8 @@ script = ExtResource("aggro_decay") [node name="AggroEvents" type="Node" parent="Systems/AggroSystem"] script = ExtResource("aggro_events") -[node name="EnemyAISystem" type="Node" parent="Systems"] -script = ExtResource("enemy_ai_system") +[node name="AISystem" type="Node" parent="Systems"] +script = ExtResource("ai_system") [node name="RespawnSystem" type="Node" parent="Systems"] script = ExtResource("respawn_system") @@ -91,12 +117,24 @@ script = ExtResource("respawn_system") [node name="SpawnSystem" type="Node" parent="Systems"] script = ExtResource("spawn_system") -[node name="EffectSystem" type="Node" parent="Systems"] -script = ExtResource("effect_system") +[node name="AuraSystem" type="Node" parent="Systems"] +script = ExtResource("aura_system") + +[node name="BuffSystem" type="Node" parent="Systems"] +script = ExtResource("buff_system") + +[node name="DebuffSystem" type="Node" parent="Systems"] +script = ExtResource("debuff_system") [node name="ElementSystem" type="Node" parent="Systems"] script = ExtResource("element_system") +[node name="HudSystem" type="Node" parent="Systems"] +script = ExtResource("hud_system") + +[node name="NameplateSystem" type="Node" parent="Systems"] +script = ExtResource("nameplate_system") + [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] navigation_mesh = SubResource("NavigationMesh_1") @@ -203,13 +241,14 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60) [node name="Enemy4d" parent="." instance=ExtResource("enemy")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60) -[node name="Boss" parent="." instance=ExtResource("boss")] +[node name="Boss" parent="." groups=["boss"] instance=ExtResource("enemy")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75) +stats = ExtResource("boss_stats") [node name="ExitGate" parent="." instance=ExtResource("gate")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4) target_scene = "res://scenes/world/world.tscn" is_exit = true -[node name="DungeonManager" type="Node" parent="."] -script = ExtResource("dungeon_manager") +[node name="DungeonSystem" type="Node" parent="Systems"] +script = ExtResource("dungeon_system") diff --git a/scenes/dungeon/dungeon_manager.gd.uid b/scenes/dungeon/dungeon_manager.gd.uid deleted file mode 100644 index 0ee401e..0000000 --- a/scenes/dungeon/dungeon_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://drn4h1lxx5t1j diff --git a/scenes/enemy/boss.gd b/scenes/enemy/boss.gd deleted file mode 100644 index d34a03f..0000000 --- a/scenes/enemy/boss.gd +++ /dev/null @@ -1,5 +0,0 @@ -extends "res://scenes/enemy/enemy.gd" - -func _ready() -> void: - super._ready() - add_to_group("boss") diff --git a/scenes/enemy/boss.gd.uid b/scenes/enemy/boss.gd.uid deleted file mode 100644 index 0397868..0000000 --- a/scenes/enemy/boss.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bkehq3dqyp2yd diff --git a/scenes/enemy/boss.tscn b/scenes/enemy/boss.tscn deleted file mode 100644 index dbfb94b..0000000 --- a/scenes/enemy/boss.tscn +++ /dev/null @@ -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"] diff --git a/scenes/enemy/boss_stats.gd.uid b/scenes/enemy/boss_stats.gd.uid index 83957d1..8592b7c 100644 --- a/scenes/enemy/boss_stats.gd.uid +++ b/scenes/enemy/boss_stats.gd.uid @@ -1 +1 @@ -uid://bio01w2gd5e7q +uid://dlawq281oesnf diff --git a/scenes/enemy/detection.gd b/scenes/enemy/detection.gd new file mode 100644 index 0000000..23efbde --- /dev/null +++ b/scenes/enemy/detection.gd @@ -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) diff --git a/scenes/enemy/detection.gd.uid b/scenes/enemy/detection.gd.uid new file mode 100644 index 0000000..9236bb4 --- /dev/null +++ b/scenes/enemy/detection.gd.uid @@ -0,0 +1 @@ +uid://b07aajhufqvb3 diff --git a/scenes/enemy/enemy.gd b/scenes/enemy/enemy.gd deleted file mode 100644 index 7f57316..0000000 --- a/scenes/enemy/enemy.gd +++ /dev/null @@ -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) diff --git a/scenes/enemy/enemy.gd.uid b/scenes/enemy/enemy.gd.uid deleted file mode 100644 index 8a49421..0000000 --- a/scenes/enemy/enemy.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwi75jx0agktd diff --git a/scenes/enemy/enemy.tscn b/scenes/enemy/enemy.tscn index 35c48a2..07d319c 100644 --- a/scenes/enemy/enemy.tscn +++ b/scenes/enemy/enemy.tscn @@ -1,18 +1,26 @@ -[gd_scene load_steps=6 format=3] +[gd_scene format=3 uid="uid://db8pa55ev4l4a"] -[ext_resource type="Script" path="res://scenes/enemy/enemy.gd" id="1"] -[ext_resource type="Script" path="res://scenes/healthbar.gd" id="4"] -[ext_resource type="Script" path="res://scenes/enemy/enemy_movement.gd" id="5"] -[ext_resource type="Resource" path="res://scenes/enemy/enemy_stats.tres" id="8"] +[ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"] +[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"] +[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] radius = 0.4 height = 1.5 +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.8, 0.1, 0.1, 1) + +[sub_resource type="SphereMesh" id="SphereMesh_1"] +material = SubResource("StandardMaterial3D_1") + [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"] radius = 0.4 height = 1.5 +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 10.0 + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] bg_color = Color(0.3, 0.1, 0.1, 1) @@ -25,74 +33,58 @@ bg_color = Color(0.1, 0.1, 0.3, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] bg_color = Color(0.2, 0.5, 0.9, 1) -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] -albedo_color = Color(0.8, 0.1, 0.1, 1) - -[sub_resource type="SphereMesh" id="SphereMesh_1"] -radius = 0.5 -height = 1.0 -material = SubResource("StandardMaterial3D_1") - -[sub_resource type="SphereShape3D" id="SphereShape3D_1"] -radius = 10.0 - -[node name="Enemy" type="CharacterBody3D"] +[node name="Enemy" type="CharacterBody3D" unique_id=1724620529] script = ExtResource("1") stats = ExtResource("8") -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038] shape = SubResource("CapsuleShape3D_1") -[node name="Mesh" type="MeshInstance3D" parent="."] +[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1598094615] mesh = SubResource("SphereMesh_1") -[node name="HitArea" type="Area3D" parent="."] +[node name="HitArea" type="Area3D" parent="." unique_id=893463784] collision_layer = 4 collision_mask = 0 -[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962] shape = SubResource("CapsuleShape3D_2") -[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] +[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945] -[node name="EnemyMovement" type="Node" parent="."] -script = ExtResource("5") +[node name="Detection" type="Node" parent="." unique_id=534240144] +script = ExtResource("2") -[node name="DetectionArea" type="Area3D" parent="."] +[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598] collision_layer = 0 -collision_mask = 1 -monitoring = true -[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347] shape = SubResource("SphereShape3D_1") -[node name="Healthbar" type="Sprite3D" parent="."] +[node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) billboard = 1 -pixel_size = 0.01 -script = ExtResource("4") -[node name="SubViewport" type="SubViewport" parent="Healthbar"] +[node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718] transparent_bg = true size = Vector2i(104, 29) -[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"] +[node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848] offset_right = 104.0 offset_bottom = 29.0 color = Color(1, 0.9, 0.2, 1) -[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"] +[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403] offset_left = 2.0 offset_top = 2.0 offset_right = 102.0 offset_bottom = 12.0 theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") -max_value = 100.0 value = 100.0 show_percentage = false -[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport"] +[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036] offset_left = 2.0 offset_top = 15.0 offset_right = 102.0 @@ -103,5 +95,5 @@ max_value = 50.0 value = 50.0 show_percentage = false -[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"] -[connection signal="body_exited" from="DetectionArea" to="." method="_on_detection_area_body_exited"] +[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"] +[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"] diff --git a/scenes/enemy/enemy_movement.gd b/scenes/enemy/enemy_movement.gd deleted file mode 100644 index 96031a2..0000000 --- a/scenes/enemy/enemy_movement.gd +++ /dev/null @@ -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) diff --git a/scenes/enemy/enemy_movement.gd.uid b/scenes/enemy/enemy_movement.gd.uid deleted file mode 100644 index 08cbb99..0000000 --- a/scenes/enemy/enemy_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://tnx6rbnnngn diff --git a/scenes/enemy/init.gd b/scenes/enemy/init.gd new file mode 100644 index 0000000..348641c --- /dev/null +++ b/scenes/enemy/init.gd @@ -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() diff --git a/scenes/enemy/init.gd.uid b/scenes/enemy/init.gd.uid new file mode 100644 index 0000000..14f802f --- /dev/null +++ b/scenes/enemy/init.gd.uid @@ -0,0 +1 @@ +uid://vy6hyqok0p8b diff --git a/scenes/healthbar.gd b/scenes/healthbar.gd deleted file mode 100644 index a38a2b4..0000000 --- a/scenes/healthbar.gd +++ /dev/null @@ -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 diff --git a/scenes/healthbar.gd.uid b/scenes/healthbar.gd.uid deleted file mode 100644 index 572c7af..0000000 --- a/scenes/healthbar.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d1w7vm7t3k3ts diff --git a/scenes/hud/hud.gd.uid b/scenes/hud/hud.gd.uid deleted file mode 100644 index 56b12aa..0000000 --- a/scenes/hud/hud.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4jhr8k4uwoy7 diff --git a/scenes/hud/hud.tscn b/scenes/hud/hud.tscn index c0aa8c0..d24a6af 100644 --- a/scenes/hud/hud.tscn +++ b/scenes/hud/hud.tscn @@ -1,7 +1,5 @@ [gd_scene format=3] -[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"] - [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"] bg_color = Color(0.2, 0.2, 0.2, 0.8) border_width_bottom = 2 @@ -34,8 +32,7 @@ bg_color = Color(0.1, 0.1, 0.3, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] bg_color = Color(0.2, 0.5, 0.9, 1) -[node name="HUD" type="CanvasLayer"] -script = ExtResource("1") +[node name="HUD" type="CanvasLayer" groups=["hud"]] [node name="HealthBar" type="ProgressBar" parent="."] offset_left = 10.0 diff --git a/scenes/player/combat.gd b/scenes/player/ability.gd similarity index 79% rename from scenes/player/combat.gd rename to scenes/player/ability.gd index 6516da1..28f1d87 100644 --- a/scenes/player/combat.gd +++ b/scenes/player/ability.gd @@ -5,5 +5,5 @@ extends Node func _unhandled_input(event: InputEvent) -> void: for i in range(5): if event.is_action_pressed("ability_%s" % (i + 1)): - EventBus.ability_use_requested.emit(player, i) + EventBus.ability_use.emit(player, i) return diff --git a/scenes/player/ability.gd.uid b/scenes/player/ability.gd.uid new file mode 100644 index 0000000..37b43ab --- /dev/null +++ b/scenes/player/ability.gd.uid @@ -0,0 +1 @@ +uid://hh5yw7vcjdqr diff --git a/scenes/player/combat.gd.uid b/scenes/player/combat.gd.uid deleted file mode 100644 index 1b28400..0000000 --- a/scenes/player/combat.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d15til6fsxw5b diff --git a/scenes/player/init.gd b/scenes/player/init.gd new file mode 100644 index 0000000..dfdf588 --- /dev/null +++ b/scenes/player/init.gd @@ -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() diff --git a/scenes/player/init.gd.uid b/scenes/player/init.gd.uid new file mode 100644 index 0000000..9e283c7 --- /dev/null +++ b/scenes/player/init.gd.uid @@ -0,0 +1 @@ +uid://cx6k5473yxno diff --git a/scenes/player/movement.gd b/scenes/player/movement.gd index bc37858..24c32bf 100644 --- a/scenes/player/movement.gd +++ b/scenes/player/movement.gd @@ -8,12 +8,8 @@ func _physics_process(delta: float) -> void: if not player.is_on_floor(): player.velocity.y -= gravity * delta - var base: BaseStats = Stats.get_base(player) - var speed: float = base.speed if base is PlayerStats else 5.0 - var jump_velocity: float = base.jump_velocity if base is PlayerStats else 4.5 - if Input.is_action_just_pressed("ui_accept") and player.is_on_floor(): - player.velocity.y = jump_velocity + player.velocity.y = PlayerData.jump_velocity var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back") var camera_pivot := player.get_node("CameraPivot") as Node3D @@ -27,10 +23,10 @@ func _physics_process(delta: float) -> void: var direction := (forward * -input_dir.y + right * input_dir.x).normalized() if direction: - player.velocity.x = direction.x * speed - player.velocity.z = direction.z * speed + player.velocity.x = direction.x * PlayerData.speed + player.velocity.z = direction.z * PlayerData.speed else: - player.velocity.x = move_toward(player.velocity.x, 0, speed) - player.velocity.z = move_toward(player.velocity.z, 0, speed) + player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed) + player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed) player.move_and_slide() diff --git a/scenes/player/player.gd b/scenes/player/player.gd deleted file mode 100644 index 2367fdf..0000000 --- a/scenes/player/player.gd +++ /dev/null @@ -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) diff --git a/scenes/player/player.gd.uid b/scenes/player/player.gd.uid deleted file mode 100644 index a695d0e..0000000 --- a/scenes/player/player.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bfpt2p7uucfyb diff --git a/scenes/player/player.tscn b/scenes/player/player.tscn index d64fc17..f0f603a 100644 --- a/scenes/player/player.tscn +++ b/scenes/player/player.tscn @@ -1,15 +1,12 @@ [gd_scene format=3 uid="uid://cdnkbt1f0db7e"] -[ext_resource type="Script" uid="uid://bfpt2p7uucfyb" path="res://scenes/player/player.gd" id="1"] +[ext_resource type="Script" uid="uid://cx6k5473yxno" path="res://scenes/player/init.gd" id="1"] [ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"] [ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"] -[ext_resource type="Script" uid="uid://d15til6fsxw5b" path="res://scenes/player/combat.gd" id="4"] +[ext_resource type="Script" uid="uid://hh5yw7vcjdqr" path="res://scenes/player/ability.gd" id="4"] [ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"] -[ext_resource type="Script" path="res://scenes/player/role/role.gd" id="10"] -[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="11"] -[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="12"] -[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="13"] -[ext_resource type="Resource" path="res://scenes/player/player_stats.tres" id="14"] +[ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"] +[ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] radius = 0.3 @@ -19,34 +16,31 @@ height = 1.8 radius = 0.3 height = 1.8 -[node name="Player" type="CharacterBody3D" unique_id=1350215040] +[node name="Player" type="CharacterBody3D" unique_id=197716516] script = ExtResource("1") stats = ExtResource("14") -[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999] +[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=311205642] shape = SubResource("CapsuleShape3D_1") -[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1346931672] +[node name="Mesh" type="MeshInstance3D" parent="." unique_id=1514179122] mesh = SubResource("CapsuleMesh_1") -[node name="CameraPivot" type="Node3D" parent="." unique_id=1292689540] +[node name="CameraPivot" type="Node3D" parent="." unique_id=1881685457] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) script = ExtResource("2") -[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=1225820651] +[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=2062990383] transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5) -[node name="Movement" type="Node" parent="." unique_id=654979387] +[node name="Movement" type="Node" parent="." unique_id=811179177] script = ExtResource("3") -[node name="Combat" type="Node" parent="." unique_id=1754235583] +[node name="Ability" type="Node" parent="." unique_id=1184596245] script = ExtResource("4") -[node name="Targeting" type="Node" parent="." unique_id=592540710] +[node name="Targeting" type="Node" parent="." unique_id=1974574662] script = ExtResource("8") -[node name="Role" type="Node" parent="." unique_id=134158295] +[node name="Role" type="Node" parent="." unique_id=1637643687] script = ExtResource("10") -tank_set = ExtResource("11") -damage_set = ExtResource("12") -healer_set = ExtResource("13") diff --git a/scenes/player/role/role.gd b/scenes/player/role/role.gd index 81f24dd..05c21df 100644 --- a/scenes/player/role/role.gd +++ b/scenes/player/role/role.gd @@ -1,40 +1,14 @@ extends Node -enum Role { TANK, DAMAGE, HEALER } - -var current_role: int = Role.DAMAGE - -@export var tank_set: AbilitySet -@export var damage_set: AbilitySet -@export var healer_set: AbilitySet - @onready var player: CharacterBody3D = get_parent() func _ready() -> void: - set_role.call_deferred(current_role) + EventBus.role_change_requested.emit(player, PlayerData.current_role) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("class_tank"): - set_role(Role.TANK) + EventBus.role_change_requested.emit(player, PlayerData.Role.TANK) elif event.is_action_pressed("class_damage"): - set_role(Role.DAMAGE) + EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE) elif event.is_action_pressed("class_healer"): - set_role(Role.HEALER) - -func set_role(new_role: int) -> void: - current_role = new_role - EventBus.role_changed.emit(player, current_role) - -func get_role_icon() -> String: - match current_role: - Role.TANK: return "T" - Role.DAMAGE: return "D" - Role.HEALER: return "H" - return "" - -func get_ability_set() -> AbilitySet: - match current_role: - Role.TANK: return tank_set - Role.DAMAGE: return damage_set - Role.HEALER: return healer_set - return damage_set + EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER) diff --git a/scenes/player/targeting.gd b/scenes/player/targeting.gd index d8db905..0e0b704 100644 --- a/scenes/player/targeting.gd +++ b/scenes/player/targeting.gd @@ -1,27 +1,12 @@ extends Node const TARGET_RANGE := 20.0 -const COMBAT_TIMEOUT := 3.0 -var current_target: Node3D = null var mouse_press_pos: Vector2 = Vector2.ZERO -var in_combat := false -var combat_timer := 0.0 @onready var player: CharacterBody3D = get_parent() @onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D") -func _ready() -> void: - EventBus.damage_dealt.connect(_on_damage_dealt) - EventBus.entity_died.connect(_on_entity_died) - EventBus.enemy_engaged.connect(_on_enemy_engaged) - -func _process(delta: float) -> void: - if in_combat: - combat_timer -= delta - if combat_timer <= 0: - in_combat = false - func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: @@ -44,71 +29,19 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void: var result := space.intersect_ray(query) if result: var hit_target := result.collider.get_parent() as Node3D - set_target(hit_target) + EventBus.target_requested.emit(player, hit_target) else: - set_target(null) + EventBus.target_requested.emit(player, null) func _cycle_target() -> void: var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") if targets.is_empty(): - set_target(null) + EventBus.target_requested.emit(player, null) return - if current_target == null or current_target not in targets: - set_target(targets[0]) + var current: Node3D = PlayerData.target + if current == null or current not in targets: + EventBus.target_requested.emit(player, targets[0]) return - var idx := targets.find(current_target) + var idx := targets.find(current) var next_idx := (idx + 1) % targets.size() - set_target(targets[next_idx]) - -func set_target(target: Node3D) -> void: - current_target = target - EventBus.target_changed.emit(player, target) - -func _on_enemy_engaged(_enemy: Node, target: Node) -> void: - if target == player: - combat_timer = COMBAT_TIMEOUT - if not in_combat: - in_combat = true - if current_target == null: - _auto_target() - -func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void: - if target == player: - combat_timer = COMBAT_TIMEOUT - if not in_combat: - in_combat = true - if current_target == null: - _auto_target() - elif attacker == player and in_combat: - combat_timer = COMBAT_TIMEOUT - -func _on_entity_died(entity: Node) -> void: - if entity == current_target: - set_target(null) - if in_combat: - _auto_target(entity) - -func _auto_target(exclude: Node = null) -> void: - # Priorität 1: Nächster Gegner - var enemies := get_tree().get_nodes_in_group("enemies") - var nearest: Node3D = null - var nearest_dist: float = INF - for enemy in enemies: - if is_instance_valid(enemy) and enemy != exclude: - var dist: float = player.global_position.distance_to(enemy.global_position) - if dist < nearest_dist: - nearest_dist = dist - nearest = enemy - if nearest: - set_target(nearest) - return - # Priorität 2: Nächstes Portal - var portals := get_tree().get_nodes_in_group("portals") - for p in portals: - if is_instance_valid(p) and p != exclude: - var dist: float = player.global_position.distance_to(p.global_position) - if dist < nearest_dist: - nearest_dist = dist - nearest = p - if nearest: - set_target(nearest) + EventBus.target_requested.emit(player, targets[next_idx]) diff --git a/scenes/portal/gate.gd b/scenes/portal/gate.gd index 5b0c0dd..1200f48 100644 --- a/scenes/portal/gate.gd +++ b/scenes/portal/gate.gd @@ -7,7 +7,7 @@ var active := false func _ready() -> void: if not is_exit: - if GameState.dungeon_cleared: + if PlayerData.dungeon_cleared: queue_free() return get_tree().create_timer(0.5).timeout.connect(_check_overlapping) @@ -23,11 +23,11 @@ func _on_gate_area_body_entered(body: Node3D) -> void: if not active: return if body is CharacterBody3D and body.name == "Player": - GameState.save_player(body) + PlayerData.save_cache() if is_exit: - GameState.returning_from_dungeon = true + PlayerData.returning_from_dungeon = true else: - GameState.portal_position = global_position + PlayerData.portal_position = global_position call_deferred("_change_scene") func _change_scene() -> void: diff --git a/scenes/portal/init.gd b/scenes/portal/init.gd new file mode 100644 index 0000000..6c98a8b --- /dev/null +++ b/scenes/portal/init.gd @@ -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) diff --git a/scenes/portal/init.gd.uid b/scenes/portal/init.gd.uid new file mode 100644 index 0000000..1a86cac --- /dev/null +++ b/scenes/portal/init.gd.uid @@ -0,0 +1 @@ +uid://dlexijybbqxop diff --git a/scenes/portal/portal.gd b/scenes/portal/portal.gd deleted file mode 100644 index 0b3f1a4..0000000 --- a/scenes/portal/portal.gd +++ /dev/null @@ -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) diff --git a/scenes/portal/portal.gd.uid b/scenes/portal/portal.gd.uid deleted file mode 100644 index 06e64cd..0000000 --- a/scenes/portal/portal.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://byjxj4mq84gki diff --git a/scenes/portal/portal.tscn b/scenes/portal/portal.tscn index eebed0d..a039e73 100644 --- a/scenes/portal/portal.tscn +++ b/scenes/portal/portal.tscn @@ -1,7 +1,6 @@ [gd_scene format=3] -[ext_resource type="Script" path="res://scenes/portal/portal.gd" id="1"] -[ext_resource type="Script" path="res://scenes/healthbar.gd" id="3"] +[ext_resource type="Script" path="res://scenes/portal/init.gd" id="1"] [ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"] [sub_resource type="SphereShape3D" id="SphereShape3D_1"] @@ -59,7 +58,6 @@ shape = SubResource("SphereShape3D_1") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) billboard = 1 pixel_size = 0.01 -script = ExtResource("3") [node name="SubViewport" type="SubViewport" parent="Healthbar"] transparent_bg = true diff --git a/scenes/world/portal_spawner.gd b/scenes/world/portal_spawner.gd index 7e13311..e0bb415 100644 --- a/scenes/world/portal_spawner.gd +++ b/scenes/world/portal_spawner.gd @@ -11,17 +11,17 @@ var portals: Array[Node] = [] var timer := 0.0 func _ready() -> void: - if GameState.portal_position != Vector3.ZERO and not GameState.dungeon_cleared: + if PlayerData.portal_position != Vector3.ZERO and not PlayerData.dungeon_cleared: call_deferred("_restore_gate") else: - if GameState.dungeon_cleared: - GameState.clear() + if PlayerData.dungeon_cleared: + PlayerData.clear_cache() call_deferred("_spawn_portal") func _restore_gate() -> void: var gate: Node3D = GATE_SCENE.instantiate() get_parent().add_child(gate) - gate.global_position = GameState.portal_position + gate.global_position = PlayerData.portal_position func _process(delta: float) -> void: timer += delta diff --git a/scenes/world/world.tscn b/scenes/world/world.tscn index 0dceb79..3c9e00b 100644 --- a/scenes/world/world.tscn +++ b/scenes/world/world.tscn @@ -1,22 +1,34 @@ [gd_scene format=3 uid="uid://dy1icabu2ssbw"] -[ext_resource type="Script" uid="uid://h0hts425epc6" path="res://systems/ability_system.gd" id="ability_system"] -[ext_resource type="Script" uid="uid://cysg30lud2ta2" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] -[ext_resource type="Script" uid="uid://cyffo1g4uhmwh" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] -[ext_resource type="Script" uid="uid://cm7ehl2pexcst" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] -[ext_resource type="Script" uid="uid://c7gsu2qddsor6" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] -[ext_resource type="Script" uid="uid://ddos7mo8rahou" path="res://systems/cooldown_system.gd" id="cooldown_system"] -[ext_resource type="Script" uid="uid://cbd1bryh0e2dw" path="res://systems/damage_system.gd" id="damage_system"] -[ext_resource type="Script" uid="uid://drdlh6tq0dfwo" path="res://systems/effect_system.gd" id="effect_system"] -[ext_resource type="Script" uid="uid://bqebxfvticxto" path="res://systems/element_system.gd" id="element_system"] -[ext_resource type="Script" uid="uid://bwhxu5586lc1l" path="res://systems/enemy_ai_system.gd" id="enemy_ai_system"] -[ext_resource type="Script" uid="uid://b3wkn5118dimy" path="res://systems/health_system.gd" id="health_system"] +[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"] +[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] +[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] +[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] +[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] +[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"] +[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"] +[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] +[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] +[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"] +[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"] +[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"] +[ext_resource type="Script" path="res://systems/portal_system.gd" id="portal_system"] +[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"] +[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"] +[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"] +[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"] +[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"] +[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"] +[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"] +[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"] +[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] +[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] [ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] -[ext_resource type="Script" uid="uid://cskx6o07iukwh" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"] -[ext_resource type="Script" uid="uid://b1qkvoqvmd21h" path="res://systems/respawn_system.gd" id="respawn_system"] -[ext_resource type="Script" uid="uid://rsnpuf77o0sn" path="res://systems/shield_system.gd" id="shield_system"] -[ext_resource type="Script" uid="uid://c84voxmnaifyt" path="res://systems/spawn_system.gd" id="spawn_system"] +[ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"] +[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"] +[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"] +[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"] [sub_resource type="NavigationMesh" id="NavigationMesh_1"] vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5) @@ -53,81 +65,111 @@ size = Vector3(5, 3, 5) [sub_resource type="BoxShape3D" id="BoxShape3D_tavern"] size = Vector3(5, 3, 5) -[node name="World" type="Node3D" unique_id=1865233338] +[node name="World" type="Node3D"] -[node name="Systems" type="Node" parent="." unique_id=1813416478] +[node name="Systems" type="Node" parent="."] -[node name="HealthSystem" type="Node" parent="Systems" unique_id=221270411] +[node name="HealthSystem" type="Node" parent="Systems"] script = ExtResource("health_system") -[node name="ShieldSystem" type="Node" parent="Systems" unique_id=1790230220] -script = ExtResource("shield_system") - -[node name="DamageSystem" type="Node" parent="Systems" unique_id=2146323526] +[node name="DamageSystem" type="Node" parent="Systems"] script = ExtResource("damage_system") -[node name="AbilitySystem" type="Node" parent="Systems" unique_id=391120092] +[node name="HealSystem" type="Node" parent="Systems"] +script = ExtResource("heal_system") + +[node name="ShieldSystem" type="Node" parent="Systems"] +script = ExtResource("shield_system") + +[node name="RoleSystem" type="Node" parent="Systems"] +script = ExtResource("role_system") +tank_set = ExtResource("tank_set") +damage_set = ExtResource("damage_set") +healer_set = ExtResource("healer_set") + +[node name="AbilitySystem" type="Node" parent="Systems"] script = ExtResource("ability_system") -[node name="CooldownSystem" type="Node" parent="Systems" unique_id=99457358] +[node name="AttackSystem" type="Node" parent="Systems"] +script = ExtResource("attack_system") + +[node name="CooldownSystem" type="Node" parent="Systems"] script = ExtResource("cooldown_system") -[node name="AggroSystem" type="Node" parent="Systems" unique_id=1539448343] +[node name="TargetingSystem" type="Node" parent="Systems"] +script = ExtResource("targeting_system") + +[node name="AggroSystem" type="Node" parent="Systems"] script = ExtResource("aggro_system") -[node name="AggroTracker" type="Node" parent="Systems/AggroSystem" unique_id=1597893665] +[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"] script = ExtResource("aggro_tracker") -[node name="AggroDecay" type="Node" parent="Systems/AggroSystem" unique_id=1571705506] +[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"] script = ExtResource("aggro_decay") -[node name="AggroEvents" type="Node" parent="Systems/AggroSystem" unique_id=1936723580] +[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"] script = ExtResource("aggro_events") -[node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042] -script = ExtResource("enemy_ai_system") +[node name="AISystem" type="Node" parent="Systems"] +script = ExtResource("ai_system") -[node name="RespawnSystem" type="Node" parent="Systems" unique_id=1586865573] +[node name="RespawnSystem" type="Node" parent="Systems"] script = ExtResource("respawn_system") -[node name="SpawnSystem" type="Node" parent="Systems" unique_id=1099032666] +[node name="SpawnSystem" type="Node" parent="Systems"] script = ExtResource("spawn_system") -[node name="EffectSystem" type="Node" parent="Systems" unique_id=1219368182] -script = ExtResource("effect_system") +[node name="PortalSystem" type="Node" parent="Systems"] +script = ExtResource("portal_system") -[node name="ElementSystem" type="Node" parent="Systems" unique_id=1401212832] +[node name="AuraSystem" type="Node" parent="Systems"] +script = ExtResource("aura_system") + +[node name="BuffSystem" type="Node" parent="Systems"] +script = ExtResource("buff_system") + +[node name="DebuffSystem" type="Node" parent="Systems"] +script = ExtResource("debuff_system") + +[node name="ElementSystem" type="Node" parent="Systems"] script = ExtResource("element_system") -[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679] +[node name="HudSystem" type="Node" parent="Systems"] +script = ExtResource("hud_system") + +[node name="NameplateSystem" type="Node" parent="Systems"] +script = ExtResource("nameplate_system") + +[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506) navigation_mesh = SubResource("NavigationMesh_1") -[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=593226019] +[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"] mesh = SubResource("PlaneMesh_1") -[node name="BodenCollision" type="StaticBody3D" parent="." unique_id=1112667638] +[node name="BodenCollision" type="StaticBody3D" parent="."] -[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision" unique_id=621554623] +[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"] shape = SubResource("WorldBoundaryShape3D_1") -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1797472817] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0) shadow_enabled = true -[node name="Taverne" type="StaticBody3D" parent="." unique_id=1978646562] +[node name="Taverne" type="StaticBody3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) -[node name="Mesh" type="MeshInstance3D" parent="Taverne" unique_id=2043279810] +[node name="Mesh" type="MeshInstance3D" parent="Taverne"] mesh = SubResource("BoxMesh_tavern") -[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne" unique_id=2108564286] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne"] shape = SubResource("BoxShape3D_tavern") -[node name="Player" parent="." unique_id=585018813 instance=ExtResource("player")] +[node name="Player" parent="." instance=ExtResource("player")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5) -[node name="HUD" parent="." unique_id=763693646 instance=ExtResource("hud")] +[node name="HUD" parent="." instance=ExtResource("hud")] -[node name="PortalSpawner" type="Node" parent="." unique_id=2100621428] +[node name="PortalSpawner" type="Node" parent="."] script = ExtResource("portal_spawner") diff --git a/systems/ability_system.gd b/systems/ability_system.gd index a80f2ad..c76f820 100644 --- a/systems/ability_system.gd +++ b/systems/ability_system.gd @@ -1,124 +1,83 @@ extends Node func _ready() -> void: - EventBus.ability_use_requested.connect(_on_ability_use_requested) + EventBus.ability_use.connect(_on_ability_use) -func _process(_delta: float) -> void: - var players := get_tree().get_nodes_in_group("player") - for player in players: - if not Stats.is_alive(player): - continue - _try_auto_attack(player) - -func _try_auto_attack(player: Node) -> void: - var targeting: Node = player.get_node_or_null("Targeting") - if not targeting or not targeting.in_combat or not targeting.current_target: +func _on_ability_use(_player: Node, ability_index: int) -> void: + if not PlayerData.alive: return - if not is_instance_valid(targeting.current_target): - return - var cooldown_system: Node = get_node("../CooldownSystem") - if not cooldown_system.is_aa_ready(player): - return - var role: Node = player.get_node("Role") - var ability_set: AbilitySet = role.get_ability_set() - if not ability_set: - return - var aa_damage: float = ability_set.aa_damage - var aa_range: float = ability_set.aa_range - var aa_is_heal: bool = ability_set.aa_is_heal - var dmg: float = _apply_passive(player, aa_damage, "heal" if aa_is_heal else "damage") - if aa_is_heal: - EventBus.heal_requested.emit(player, player, dmg) - else: - var dist: float = player.global_position.distance_to(targeting.current_target.global_position) - if dist > aa_range: - return - EventBus.damage_requested.emit(player, targeting.current_target, dmg) - var player_base: BaseStats = Stats.get_base(player) - var aa_cd: float = player_base.aa_cooldown if player_base is PlayerStats else 0.5 - cooldown_system.set_aa_cooldown(player, aa_cd) - -func _on_ability_use_requested(player: Node, ability_index: int) -> void: - var role: Node = player.get_node_or_null("Role") - if not role: - return - var ability_set: AbilitySet = role.get_ability_set() + var ability_set: AbilitySet = PlayerData.ability_set if not ability_set or ability_index >= ability_set.abilities.size(): return var ability: Ability = ability_set.abilities[ability_index] if not ability or ability.type == Ability.Type.PASSIVE: return - var cooldown_system: Node = get_node("../CooldownSystem") - if not cooldown_system.is_ready(player, ability_index): + if PlayerData.cooldowns[ability_index] > 0: return - if ability.uses_gcd and not cooldown_system.is_gcd_ready(player): + if ability.uses_gcd and PlayerData.gcd > 0: return - var success: bool = _execute_ability(player, ability) + var success: bool = _execute_ability(ability) if not success: return - var player_base: BaseStats = Stats.get_base(player) - var gcd_time: float = player_base.gcd_time if player_base is PlayerStats else 0.5 - var gcd: float = gcd_time if ability.uses_gcd else 0.0 - cooldown_system.set_cooldown(player, ability_index, ability.cooldown, gcd) + var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0 + PlayerData.cooldowns[ability_index] = ability.cooldown + PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd) + if gcd > 0: + PlayerData.gcd = gcd -func _execute_ability(player: Node, ability: Ability) -> bool: - var targeting: Node = player.get_node("Targeting") +func _execute_ability(ability: Ability) -> bool: var stat: String = "heal" if ability.is_heal else "damage" - var dmg: float = _apply_passive(player, ability.damage, stat) + var dmg: float = _apply_passive(ability.damage, stat) + var player: Node = get_tree().get_first_node_in_group("player") match ability.type: Ability.Type.SINGLE: - return _execute_single(player, targeting, ability, dmg) + return _execute_single(player, ability, dmg) Ability.Type.AOE: return _execute_aoe(player, ability, dmg) Ability.Type.UTILITY: - return _execute_utility(player, ability) + return _execute_utility(ability) Ability.Type.ULT: - return _execute_ult(player, targeting, ability, dmg) + return _execute_ult(player, ability, dmg) return false -func _apply_passive(player: Node, base: float, stat: String) -> float: - var mult: Variant = Stats.get_stat(player, "buff_" + stat) - if mult != null: - return base * mult - return base +func _apply_passive(base: float, stat: String) -> float: + var mult: float = 1.0 + match stat: + "damage": mult = PlayerData.buff_damage + "heal": mult = PlayerData.buff_heal + return base * mult -func _in_range(player: Node, targeting: Node, ability: Ability) -> bool: +func _in_range(ability: Ability) -> bool: if ability.ability_range <= 0 or ability.is_heal: return true - if not is_instance_valid(targeting.current_target): + if not is_instance_valid(PlayerData.target): return false - var dist: float = player.global_position.distance_to(targeting.current_target.global_position) + var player: Node = get_tree().get_first_node_in_group("player") + var dist: float = player.global_position.distance_to(PlayerData.target.global_position) return dist <= ability.ability_range -func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: +func _execute_single(player: Node, ability: Ability, dmg: float) -> bool: if ability.is_heal: EventBus.heal_requested.emit(player, player, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return true - if not _in_range(player, targeting, ability): + if not _in_range(ability): return false - if not is_instance_valid(targeting.current_target): + if not is_instance_valid(PlayerData.target): return false - EventBus.damage_requested.emit(player, targeting.current_target, dmg) + EventBus.damage_requested.emit(player, PlayerData.target, dmg) if ability.element != 0: - EventBus.element_damage_dealt.emit(player, targeting.current_target, dmg, ability.element) + EventBus.element_damage_dealt.emit(player, PlayerData.target, dmg, ability.element) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return true func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: if ability.is_heal: EventBus.heal_requested.emit(player, player, dmg) - var players := get_tree().get_nodes_in_group("player") - for p in players: - if p != player and is_instance_valid(p): - var dist: float = player.global_position.distance_to(p.global_position) - if dist <= ability.ability_range: - EventBus.heal_requested.emit(player, p, dmg) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return true var hit := false - var enemies := get_tree().get_nodes_in_group("enemies") - for enemy in enemies: + for enemy in get_tree().get_nodes_in_group("enemies"): var dist: float = player.global_position.distance_to(enemy.global_position) if dist <= ability.ability_range: EventBus.damage_requested.emit(player, enemy, dmg) @@ -129,44 +88,35 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return hit -func _execute_utility(player: Node, ability: Ability) -> bool: - var max_shield: float = Stats.get_stat(player, "max_shield") - if max_shield <= 0: +func _execute_utility(ability: Ability) -> bool: + if PlayerData.max_shield <= 0: return false - var shield: float = Stats.get_stat(player, "shield") + var shield: float = PlayerData.shield if ability.damage > 0: - shield = max_shield * (ability.damage / 100.0) + shield = PlayerData.max_shield * (ability.damage / 100.0) else: - if shield >= max_shield: + if shield >= PlayerData.max_shield: return false - shield = max_shield - Stats.set_stat(player, "shield", shield) - EventBus.shield_changed.emit(player, shield, max_shield) + shield = PlayerData.max_shield + PlayerData.set_shield(shield) return true -func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: +func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool: if ability.is_heal: EventBus.heal_requested.emit(player, player, dmg) - var players := get_tree().get_nodes_in_group("player") var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range - for p in players: - if p != player and is_instance_valid(p): - var dist: float = player.global_position.distance_to(p.global_position) - if dist <= aoe_range: - EventBus.heal_requested.emit(player, p, dmg * 0.4) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return true - if not _in_range(player, targeting, ability): + if not _in_range(ability): return false - if not is_instance_valid(targeting.current_target): + if not is_instance_valid(PlayerData.target): return false - var target: Node3D = targeting.current_target + var target: Node3D = PlayerData.target EventBus.damage_requested.emit(player, target, dmg * 5.0) if ability.element != 0: EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element) var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range - var enemies := get_tree().get_nodes_in_group("enemies") - for enemy in enemies: + for enemy in get_tree().get_nodes_in_group("enemies"): if enemy != target and is_instance_valid(enemy): var enemy_dist: float = target.global_position.distance_to(enemy.global_position) if enemy_dist <= splash_range: diff --git a/systems/aggro/aggro_decay.gd b/systems/aggro/aggro_decay.gd index 3475cdc..e810a64 100644 --- a/systems/aggro/aggro_decay.gd +++ b/systems/aggro/aggro_decay.gd @@ -23,8 +23,12 @@ func _update_combat_timers(delta: float) -> void: func _decay_aggro(enemy: Node, delta: float) -> void: var table: Dictionary = tracker.aggro_tables[enemy] - var base: BaseStats = Stats.get_base(enemy) - var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0 + var data_source: Node = tracker._get_data_source(enemy) + var aggro_decay: float = 1.0 + if data_source: + var base: EnemyStats = data_source.get_base(enemy) + if base: + aggro_decay = base.aggro_decay for player in table.keys(): if is_in_combat(player): continue @@ -58,10 +62,11 @@ func spread_aggro(source: Node, attacker: Node, amount: float) -> void: func alert_nearby(enemy: Node, target: Node) -> void: var radius: float = tracker.get_alert_radius(enemy) for other in tracker.get_enemies_in_radius(enemy, radius): - if "state" in other and other.state == other.State.IDLE: + var data_source: Node = tracker._get_data_source(other) + if data_source and data_source.get_stat(other, "state") == 0: tracker.add_aggro(other, target, 1.0) - other.target = target - other.state = other.State.CHASE + data_source.set_stat(other, "target", target) + data_source.set_stat(other, "state", 1) EventBus.enemy_engaged.emit(other, target) func erase_entity(entity: Node) -> void: diff --git a/systems/aggro/aggro_events.gd b/systems/aggro/aggro_events.gd index a6b573a..270e39e 100644 --- a/systems/aggro/aggro_events.gd +++ b/systems/aggro/aggro_events.gd @@ -14,14 +14,16 @@ func _ready() -> void: func _on_enemy_detected(enemy: Node, player: Node) -> void: if not enemy.is_in_group("enemies"): return - if "state" in enemy: - if enemy.state == enemy.State.CHASE or enemy.state == enemy.State.ATTACK: + var data_source: Node = tracker._get_data_source(enemy) + if data_source: + var state: int = data_source.get_stat(enemy, "state") + if state == 1 or state == 2: return tracker.add_player_in_range(enemy, player) tracker.add_aggro(enemy, player, 1.0) - if "state" in enemy: - enemy.target = player - enemy.state = enemy.State.CHASE + if data_source: + data_source.set_stat(enemy, "target", player) + data_source.set_stat(enemy, "state", 1) EventBus.enemy_engaged.emit(enemy, player) decay.alert_nearby(enemy, player) @@ -33,8 +35,7 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: return decay.reset_combat_timer(attacker) var multiplier := 1.0 - var role: Node = attacker.get_node_or_null("Role") - if role and role.current_role == 0: + if PlayerData.current_role == PlayerData.Role.TANK: multiplier = config.tank_multiplier var aggro: float = amount * multiplier tracker.add_aggro(target, attacker, aggro) diff --git a/systems/aggro/aggro_tracker.gd b/systems/aggro/aggro_tracker.gd index 5dc4734..0a5b6e6 100644 --- a/systems/aggro/aggro_tracker.gd +++ b/systems/aggro/aggro_tracker.gd @@ -43,17 +43,19 @@ func get_top_target(table: Dictionary) -> Node: return top func update_target(enemy: Node) -> void: - if not "state" in enemy: - return var table: Dictionary = aggro_tables[enemy] var top: Node = get_top_target(table) - if top and top != enemy.target: - enemy.target = top - if enemy.state == enemy.State.IDLE or enemy.state == enemy.State.RETURN: - enemy.state = enemy.State.CHASE - elif not top and enemy.state != enemy.State.IDLE and enemy.state != enemy.State.RETURN: - enemy.target = null - enemy.state = enemy.State.RETURN + var data_source: Node = _get_data_source(enemy) + if not data_source: + return + var state: int = data_source.get_stat(enemy, "state") + if top and top != data_source.get_stat(enemy, "target"): + data_source.set_stat(enemy, "target", top) + if state == 0 or state == 3: + data_source.set_stat(enemy, "state", 1) + elif not top and state != 0 and state != 3: + data_source.set_stat(enemy, "target", null) + data_source.set_stat(enemy, "state", 3) func get_enemies_in_radius(source: Node, radius: float) -> Array: var result: Array = [] @@ -65,8 +67,12 @@ func get_enemies_in_radius(source: Node, radius: float) -> Array: return result func get_alert_radius(entity: Node) -> float: - var base: BaseStats = Stats.get_base(entity) - return base.alert_radius if base is EnemyStats else 10.0 + var data_source: Node = _get_data_source(entity) + if data_source: + var base: EnemyStats = data_source.get_base(entity) + if base: + return base.alert_radius + return 10.0 func erase_entity(entity: Node) -> void: aggro_tables.erase(entity) @@ -74,9 +80,17 @@ func erase_entity(entity: Node) -> void: for enemy in aggro_tables: if is_instance_valid(enemy): aggro_tables[enemy].erase(entity) - if "target" in enemy and entity == enemy.target: - enemy.target = null - enemy.state = enemy.State.RETURN + var data_source: Node = _get_data_source(enemy) + if data_source and data_source.get_stat(enemy, "target") == entity: + data_source.set_stat(enemy, "target", null) + data_source.set_stat(enemy, "state", 3) for enemy in players_in_range: if is_instance_valid(enemy): players_in_range[enemy].erase(entity) + +func _get_data_source(entity: Node) -> Node: + if entity.is_in_group("boss"): + return BossData + elif entity.is_in_group("enemies"): + return EnemyData + return null diff --git a/systems/ai_system.gd b/systems/ai_system.gd new file mode 100644 index 0000000..4c52887 --- /dev/null +++ b/systems/ai_system.gd @@ -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) diff --git a/systems/ai_system.gd.uid b/systems/ai_system.gd.uid new file mode 100644 index 0000000..f54ccbd --- /dev/null +++ b/systems/ai_system.gd.uid @@ -0,0 +1 @@ +uid://dokr1ut7ea541 diff --git a/systems/attack_system.gd b/systems/attack_system.gd new file mode 100644 index 0000000..a683f89 --- /dev/null +++ b/systems/attack_system.gd @@ -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 diff --git a/systems/attack_system.gd.uid b/systems/attack_system.gd.uid new file mode 100644 index 0000000..3766e79 --- /dev/null +++ b/systems/attack_system.gd.uid @@ -0,0 +1 @@ +uid://dvuds0uuffj6t diff --git a/systems/aura_system.gd b/systems/aura_system.gd new file mode 100644 index 0000000..ad2b8c2 --- /dev/null +++ b/systems/aura_system.gd @@ -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) diff --git a/systems/aura_system.gd.uid b/systems/aura_system.gd.uid new file mode 100644 index 0000000..98a7a87 --- /dev/null +++ b/systems/aura_system.gd.uid @@ -0,0 +1 @@ +uid://b17o3hfdm8uo6 diff --git a/systems/buff_system.gd b/systems/buff_system.gd new file mode 100644 index 0000000..01dc477 --- /dev/null +++ b/systems/buff_system.gd @@ -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"]) diff --git a/systems/buff_system.gd.uid b/systems/buff_system.gd.uid new file mode 100644 index 0000000..d11e7f8 --- /dev/null +++ b/systems/buff_system.gd.uid @@ -0,0 +1 @@ +uid://y2bm5ssu77wp diff --git a/systems/cooldown_system.gd b/systems/cooldown_system.gd index 44e605e..8f0cc02 100644 --- a/systems/cooldown_system.gd +++ b/systems/cooldown_system.gd @@ -1,73 +1,19 @@ extends Node -var cooldowns: Dictionary = {} - func _ready() -> void: - add_to_group("cooldown_system") EventBus.role_changed.connect(_on_role_changed) -func register(entity: Node, ability_count: int) -> void: - cooldowns[entity] = { - "cds": [] as Array[float], - "max_cds": [] as Array[float], - "gcd": 0.0, - "aa": 0.0, - } - cooldowns[entity]["cds"].resize(ability_count) - cooldowns[entity]["cds"].fill(0.0) - cooldowns[entity]["max_cds"].resize(ability_count) - cooldowns[entity]["max_cds"].fill(0.0) - -func deregister(entity: Node) -> void: - cooldowns.erase(entity) - func _process(delta: float) -> void: - for entity in cooldowns: - if not is_instance_valid(entity): - continue - var data: Dictionary = cooldowns[entity] - if data["gcd"] > 0: - data["gcd"] -= delta - if data["aa"] > 0: - data["aa"] -= delta - var cds: Array = data["cds"] - for i in range(cds.size()): - if cds[i] > 0: - cds[i] -= delta - EventBus.cooldown_tick.emit(cds, data["max_cds"], data["gcd"]) + if PlayerData.gcd > 0: + PlayerData.gcd -= delta + if PlayerData.aa_timer > 0: + PlayerData.aa_timer -= delta + for i in range(PlayerData.cooldowns.size()): + if PlayerData.cooldowns[i] > 0: + PlayerData.cooldowns[i] -= delta + EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.gcd) -func is_ready(entity: Node, index: int) -> bool: - if entity not in cooldowns: - return false - return cooldowns[entity]["cds"][index] <= 0 - -func is_gcd_ready(entity: Node) -> bool: - if entity not in cooldowns: - return false - return cooldowns[entity]["gcd"] <= 0 - -func is_aa_ready(entity: Node) -> bool: - if entity not in cooldowns: - return false - return cooldowns[entity]["aa"] <= 0 - -func set_cooldown(entity: Node, index: int, cd: float, gcd: float) -> void: - if entity not in cooldowns: - return - var data: Dictionary = cooldowns[entity] - data["cds"][index] = cd - data["max_cds"][index] = max(cd, gcd) - if gcd > 0: - data["gcd"] = gcd - -func set_aa_cooldown(entity: Node, cd: float) -> void: - if entity not in cooldowns: - return - cooldowns[entity]["aa"] = cd - -func _on_role_changed(player: Node, _role_type: int) -> void: - if player in cooldowns: - var data: Dictionary = cooldowns[player] - data["cds"].fill(0.0) - data["max_cds"].fill(0.0) - data["gcd"] = 0.0 +func _on_role_changed(_player: Node, _role_type: int) -> void: + PlayerData.cooldowns.fill(0.0) + PlayerData.max_cooldowns.fill(0.0) + PlayerData.gcd = 0.0 diff --git a/systems/damage_system.gd b/systems/damage_system.gd index 61510e1..58eb844 100644 --- a/systems/damage_system.gd +++ b/systems/damage_system.gd @@ -1 +1,38 @@ extends Node + +func _ready() -> void: + EventBus.damage_requested.connect(_on_damage_requested) + +func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: + var remaining: float = amount + var shield_system: Node = get_node_or_null("../ShieldSystem") + if shield_system: + remaining = shield_system.absorb(target, remaining) + EventBus.damage_dealt.emit(attacker, target, amount) + if remaining > 0: + _apply_damage(target, remaining) + +func _apply_damage(entity: Node, amount: float) -> void: + if entity == _get_player(): + var health: float = PlayerData.health - amount + if health < 0: + health = 0 + PlayerData.set_health(health) + elif entity.is_in_group("boss"): + var health: float = BossData.get_stat(entity, "health") - amount + if health < 0: + health = 0 + BossData.set_health(entity, health) + elif entity.is_in_group("enemies"): + var health: float = EnemyData.get_stat(entity, "health") - amount + if health < 0: + health = 0 + EnemyData.set_health(entity, health) + elif entity.is_in_group("portals"): + var health: float = PortalData.get_stat(entity, "health") - amount + if health < 0: + health = 0 + PortalData.set_health(entity, health) + +func _get_player() -> Node: + return get_tree().get_first_node_in_group("player") diff --git a/systems/damage_system.gd.uid b/systems/damage_system.gd.uid index 867c531..fcb128f 100644 --- a/systems/damage_system.gd.uid +++ b/systems/damage_system.gd.uid @@ -1 +1 @@ -uid://cbd1bryh0e2dw +uid://cmy1kqo1pk1q8 diff --git a/systems/debuff_system.gd b/systems/debuff_system.gd new file mode 100644 index 0000000..45042cd --- /dev/null +++ b/systems/debuff_system.gd @@ -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) diff --git a/systems/debuff_system.gd.uid b/systems/debuff_system.gd.uid new file mode 100644 index 0000000..517bb53 --- /dev/null +++ b/systems/debuff_system.gd.uid @@ -0,0 +1 @@ +uid://ce12ledregjqx diff --git a/scenes/dungeon/dungeon_manager.gd b/systems/dungeon_system.gd similarity index 58% rename from scenes/dungeon/dungeon_manager.gd rename to systems/dungeon_system.gd index 9f89f63..f6a9f94 100644 --- a/scenes/dungeon/dungeon_manager.gd +++ b/systems/dungeon_system.gd @@ -1,16 +1,13 @@ extends Node func _ready() -> void: - var player: Node = get_tree().get_first_node_in_group("player") - if player: - GameState.restore_player(player) EventBus.entity_died.connect(_on_entity_died) func _on_entity_died(entity: Node) -> void: if entity.is_in_group("boss"): await get_tree().create_timer(2.0).timeout - GameState.dungeon_cleared = true - GameState.returning_from_dungeon = false - GameState.clear() + PlayerData.dungeon_cleared = true + PlayerData.returning_from_dungeon = false + PlayerData.clear_cache() EventBus.dungeon_cleared.emit() get_tree().change_scene_to_file("res://scenes/world/world.tscn") diff --git a/systems/dungeon_system.gd.uid b/systems/dungeon_system.gd.uid new file mode 100644 index 0000000..e963515 --- /dev/null +++ b/systems/dungeon_system.gd.uid @@ -0,0 +1 @@ +uid://lc5n3uxi4fho diff --git a/systems/effect_system.gd b/systems/effect_system.gd deleted file mode 100644 index d60ae1b..0000000 --- a/systems/effect_system.gd +++ /dev/null @@ -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) diff --git a/systems/effect_system.gd.uid b/systems/effect_system.gd.uid deleted file mode 100644 index f4afc65..0000000 --- a/systems/effect_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://drdlh6tq0dfwo diff --git a/systems/enemy_ai_system.gd b/systems/enemy_ai_system.gd deleted file mode 100644 index 3b431f1..0000000 --- a/systems/enemy_ai_system.gd +++ /dev/null @@ -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 diff --git a/systems/enemy_ai_system.gd.uid b/systems/enemy_ai_system.gd.uid deleted file mode 100644 index c4470f9..0000000 --- a/systems/enemy_ai_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwhxu5586lc1l diff --git a/systems/heal_system.gd b/systems/heal_system.gd new file mode 100644 index 0000000..dc6f72f --- /dev/null +++ b/systems/heal_system.gd @@ -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") diff --git a/systems/heal_system.gd.uid b/systems/heal_system.gd.uid new file mode 100644 index 0000000..bf85d80 --- /dev/null +++ b/systems/heal_system.gd.uid @@ -0,0 +1 @@ +uid://8jyik37e4tjw diff --git a/systems/health_system.gd b/systems/health_system.gd index 156ea6e..6726a2f 100644 --- a/systems/health_system.gd +++ b/systems/health_system.gd @@ -1,14 +1,25 @@ extends Node func _ready() -> void: - EventBus.damage_requested.connect(_on_damage_requested) - EventBus.heal_requested.connect(_on_heal_requested) + _emit_initial.call_deferred() func _process(delta: float) -> void: - for entity in Stats.entities: + _regen_player(delta) + _regen_entities(delta, EnemyData.entities) + _regen_entities(delta, BossData.entities) + +func _regen_player(delta: float) -> void: + if not PlayerData.alive or PlayerData.health_regen <= 0: + return + if PlayerData.health < PlayerData.max_health: + var health: float = min(PlayerData.health + PlayerData.health_regen * delta, PlayerData.max_health) + PlayerData.set_health(health) + +func _regen_entities(delta: float, entities: Dictionary) -> void: + for entity in entities: if not is_instance_valid(entity): continue - var data: Dictionary = Stats.entities[entity] + var data: Dictionary = entities[entity] if not data["alive"]: continue var regen: float = data["health_regen"] @@ -16,34 +27,6 @@ func _process(delta: float) -> void: data["health"] = min(data["health"] + regen * delta, data["max_health"]) EventBus.health_changed.emit(entity, data["health"], data["max_health"]) -func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: - if not Stats.is_alive(target): - return - var remaining: float = amount - var shield_system: Node = get_node_or_null("../ShieldSystem") - if shield_system: - remaining = shield_system.absorb(target, remaining) - EventBus.damage_dealt.emit(attacker, target, amount) - if remaining > 0: - _take_damage(target, remaining) - -func _take_damage(entity: Node, amount: float) -> void: - var health: float = Stats.get_stat(entity, "health") - health -= amount - if health <= 0: - health = 0 - Stats.set_stat(entity, "health", health) - var max_health: float = Stats.get_stat(entity, "max_health") - EventBus.health_changed.emit(entity, health, max_health) - if health <= 0: - Stats.set_stat(entity, "alive", false) - EventBus.entity_died.emit(entity) - -func _on_heal_requested(healer: Node, target: Node, amount: float) -> void: - if not Stats.is_alive(target): - return - var health: float = Stats.get_stat(target, "health") - var max_health: float = Stats.get_stat(target, "max_health") - health = min(health + amount, max_health) - Stats.set_stat(target, "health", health) - EventBus.health_changed.emit(target, health, max_health) +func _emit_initial() -> void: + EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health) + EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield) diff --git a/systems/health_system.gd.uid b/systems/health_system.gd.uid index b86c109..3feac41 100644 --- a/systems/health_system.gd.uid +++ b/systems/health_system.gd.uid @@ -1 +1 @@ -uid://b3wkn5118dimy +uid://h362ftxb0cns diff --git a/scenes/hud/hud.gd b/systems/hud_system.gd similarity index 57% rename from scenes/hud/hud.gd rename to systems/hud_system.gd index a02c209..82addd6 100644 --- a/scenes/hud/hud.gd +++ b/systems/hud_system.gd @@ -1,75 +1,105 @@ -extends CanvasLayer +extends Node const GCD_TIME := 0.5 - -@onready var health_bar: ProgressBar = $HealthBar -@onready var health_label: Label = $HealthBar/HealthLabel -@onready var shield_bar: ProgressBar = $ShieldBar -@onready var shield_label: Label = $ShieldBar/ShieldLabel -@onready var respawn_label: Label = $RespawnTimer -@onready var class_icon: Label = $AbilityBar/ClassIcon/Label -@onready var ability_panels: Array = [ - $AbilityBar/Ability1, - $AbilityBar/Ability2, - $AbilityBar/Ability3, - $AbilityBar/Ability4, - $AbilityBar/Ability5, -] +const ICON_SIZE := 20 +const FONT_SIZE := 14 +const BORDER_WIDTH := 2 +const MARGIN := 2 var ability_labels: Array[String] = ["1", "2", "3", "4", "P"] var effect_container: HBoxContainer = null func _ready() -> void: - respawn_label.visible = false - _create_effect_container() EventBus.health_changed.connect(_on_health_changed) EventBus.shield_changed.connect(_on_shield_changed) EventBus.entity_died.connect(_on_entity_died) EventBus.player_respawned.connect(_on_player_respawned) - EventBus.role_changed.connect(_on_role_changed) EventBus.respawn_tick.connect(_on_respawn_tick) + EventBus.role_changed.connect(_on_role_changed) EventBus.cooldown_tick.connect(_on_cooldown_tick) EventBus.effect_applied.connect(_on_effect_applied) EventBus.effect_expired.connect(_on_effect_expired) + _init_hud.call_deferred() + +func _init_hud() -> void: + var hud: CanvasLayer = _get_hud() + if not hud: + return + hud.get_node("RespawnTimer").visible = false + effect_container = HBoxContainer.new() + effect_container.name = "EffectContainer" + effect_container.position = Vector2(10, 60) + effect_container.add_theme_constant_override("separation", 3) + hud.add_child(effect_container) func _on_health_changed(entity: Node, current: float, max_val: float) -> void: - if entity.name == "Player": - health_bar.max_value = max_val - health_bar.value = current - health_label.text = "%d/%d" % [current, max_val] + if entity != PlayerData: + return + var hud: CanvasLayer = _get_hud() + if not hud: + return + var bar: ProgressBar = hud.get_node("HealthBar") + bar.max_value = max_val + bar.value = current + hud.get_node("HealthBar/HealthLabel").text = "%d/%d" % [current, max_val] func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: - if entity.name == "Player": - shield_bar.max_value = max_val - shield_bar.value = current - shield_label.text = "%d/%d" % [current, max_val] + if entity != PlayerData: + return + var hud: CanvasLayer = _get_hud() + if not hud: + return + var bar: ProgressBar = hud.get_node("ShieldBar") + bar.max_value = max_val + bar.value = current + hud.get_node("ShieldBar/ShieldLabel").text = "%d/%d" % [current, max_val] func _on_entity_died(entity: Node) -> void: - if entity.name == "Player": - respawn_label.visible = true + if entity != PlayerData: + return + var hud: CanvasLayer = _get_hud() + if hud: + hud.get_node("RespawnTimer").visible = true func _on_player_respawned(_player: Node) -> void: - respawn_label.visible = false + var hud: CanvasLayer = _get_hud() + if hud: + hud.get_node("RespawnTimer").visible = false func _on_respawn_tick(timer: float) -> void: - respawn_label.text = str(ceil(timer)) + var hud: CanvasLayer = _get_hud() + if hud: + hud.get_node("RespawnTimer").text = str(ceil(timer)) func _on_role_changed(_player: Node, role_type: int) -> void: + var hud: CanvasLayer = _get_hud() + if not hud: + return + var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label") match role_type: - 0: class_icon.text = "T" - 1: class_icon.text = "D" - 2: class_icon.text = "H" + 0: icon.text = "T" + 1: icon.text = "D" + 2: icon.text = "H" func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void: - for i in range(min(ability_panels.size(), cooldowns.size())): - var panel: Panel = ability_panels[i] + var hud: CanvasLayer = _get_hud() + if not hud: + return + var panels: Array = [ + hud.get_node("AbilityBar/Ability1"), + hud.get_node("AbilityBar/Ability2"), + hud.get_node("AbilityBar/Ability3"), + hud.get_node("AbilityBar/Ability4"), + hud.get_node("AbilityBar/Ability5"), + ] + for i in range(min(panels.size(), cooldowns.size())): + var panel: Panel = panels[i] var label: Label = panel.get_node("Label") var overlay: ColorRect = panel.get_node("CooldownOverlay") var cd: float = cooldowns[i] var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0 var active_cd: float = max(cd, gcd) var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME - if active_cd > 0: var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0) overlay.visible = true @@ -79,43 +109,19 @@ func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) overlay.visible = false label.text = ability_labels[i] -func _create_effect_container() -> void: - effect_container = HBoxContainer.new() - effect_container.name = "EffectContainer" - effect_container.position = Vector2(10, 60) - effect_container.add_theme_constant_override("separation", 3) - add_child(effect_container) - func _on_effect_applied(target: Node, effect: Effect) -> void: - if target.name != "Player": + if target != PlayerData: return - var panel := _create_icon_panel(effect) - var insert_idx: int = _get_sorted_index(effect.type) - effect_container.add_child(panel) - effect_container.move_child(panel, insert_idx) + if effect_container: + _add_icon(effect) func _on_effect_expired(target: Node, effect: Effect) -> void: - if target.name != "Player": + if target != PlayerData: return - for child in effect_container.get_children(): - if child.has_meta("effect_type") and child.has_meta("effect_name"): - if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name: - child.queue_free() - return + if effect_container: + _remove_icon(effect) -func _get_sorted_index(type: int) -> int: - var idx := 0 - for child in effect_container.get_children(): - if not child.has_meta("effect_type"): - continue - var child_type: int = child.get_meta("effect_type") - if child_type <= type: - idx += 1 - else: - break - return idx - -func _create_icon_panel(effect: Effect) -> PanelContainer: +func _add_icon(effect: Effect) -> void: var panel := PanelContainer.new() var style := StyleBoxFlat.new() match effect.type: @@ -128,18 +134,35 @@ func _create_icon_panel(effect: Effect) -> PanelContainer: Effect.Type.DEBUFF: style.bg_color = Color(0.3, 0.15, 0.15, 1) style.border_color = Color(1.0, 0.3, 0.3, 1) - style.set_border_width_all(2) - style.set_content_margin_all(2) + style.set_border_width_all(BORDER_WIDTH) + style.set_content_margin_all(MARGIN) panel.add_theme_stylebox_override("panel", style) var label := Label.new() label.text = effect.effect_name.left(1) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - label.add_theme_font_size_override("font_size", 14) + label.add_theme_font_size_override("font_size", FONT_SIZE) label.add_theme_color_override("font_color", Color.WHITE) - label.custom_minimum_size = Vector2(20, 20) + label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE) panel.add_child(label) - panel.custom_minimum_size = Vector2(24, 24) + panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2) panel.set_meta("effect_type", effect.type) panel.set_meta("effect_name", effect.effect_name) - return panel + var insert_idx := 0 + for child in effect_container.get_children(): + if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type: + insert_idx += 1 + else: + break + effect_container.add_child(panel) + effect_container.move_child(panel, insert_idx) + +func _remove_icon(effect: Effect) -> void: + for child in effect_container.get_children(): + if child.has_meta("effect_type") and child.has_meta("effect_name"): + if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name: + child.queue_free() + return + +func _get_hud() -> CanvasLayer: + return get_tree().get_first_node_in_group("hud") as CanvasLayer diff --git a/systems/hud_system.gd.uid b/systems/hud_system.gd.uid new file mode 100644 index 0000000..919bef8 --- /dev/null +++ b/systems/hud_system.gd.uid @@ -0,0 +1 @@ +uid://da87wrxxuhws1 diff --git a/systems/nameplate_system.gd b/systems/nameplate_system.gd new file mode 100644 index 0000000..e802394 --- /dev/null +++ b/systems/nameplate_system.gd @@ -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 diff --git a/systems/nameplate_system.gd.uid b/systems/nameplate_system.gd.uid new file mode 100644 index 0000000..6d2d652 --- /dev/null +++ b/systems/nameplate_system.gd.uid @@ -0,0 +1 @@ +uid://yijhaxo8anul diff --git a/systems/portal_system.gd b/systems/portal_system.gd new file mode 100644 index 0000000..20a2223 --- /dev/null +++ b/systems/portal_system.gd @@ -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() diff --git a/systems/portal_system.gd.uid b/systems/portal_system.gd.uid new file mode 100644 index 0000000..acb14b0 --- /dev/null +++ b/systems/portal_system.gd.uid @@ -0,0 +1 @@ +uid://c5sqw08twxtnh diff --git a/systems/respawn_system.gd b/systems/respawn_system.gd index 765eb8b..21cae83 100644 --- a/systems/respawn_system.gd +++ b/systems/respawn_system.gd @@ -1,48 +1,42 @@ extends Node -var dead_players: Dictionary = {} +var respawn_timer := 0.0 +var is_dead := false func _ready() -> void: EventBus.entity_died.connect(_on_entity_died) func _process(delta: float) -> void: - for player in dead_players.keys(): - if not is_instance_valid(player): - dead_players.erase(player) - continue - dead_players[player] -= delta - EventBus.respawn_tick.emit(dead_players[player]) - if dead_players[player] <= 0: - _respawn(player) + if not is_dead: + return + respawn_timer -= delta + EventBus.respawn_tick.emit(respawn_timer) + if respawn_timer <= 0: + _respawn() func _on_entity_died(entity: Node) -> void: if not entity.is_in_group("player"): return - if entity in dead_players: + if is_dead: return - var base: BaseStats = Stats.get_base(entity) - var respawn_time: float = base.respawn_time if base is PlayerStats else 3.0 - dead_players[entity] = respawn_time + is_dead = true + respawn_timer = PlayerData.respawn_time entity.velocity = Vector3.ZERO entity.get_node("Mesh").visible = false entity.get_node("CollisionShape3D").disabled = true entity.get_node("Movement").set_physics_process(false) - entity.get_node("Combat").set_process_unhandled_input(false) + entity.get_node("Ability").set_process_unhandled_input(false) entity.get_node("Targeting").set_process_unhandled_input(false) -func _respawn(player: Node) -> void: - dead_players.erase(player) +func _respawn() -> void: + is_dead = false + var player: Node = get_tree().get_first_node_in_group("player") + if not player: + return player.global_position = Vector3(0, 1, -5) player.get_node("Mesh").visible = true player.get_node("CollisionShape3D").disabled = false player.get_node("Movement").set_physics_process(true) - player.get_node("Combat").set_process_unhandled_input(true) + player.get_node("Ability").set_process_unhandled_input(true) player.get_node("Targeting").set_process_unhandled_input(true) - var max_health: float = Stats.get_stat(player, "max_health") - var max_shield: float = Stats.get_stat(player, "max_shield") - Stats.set_stat(player, "health", max_health) - Stats.set_stat(player, "shield", max_shield) - Stats.set_stat(player, "alive", true) - EventBus.health_changed.emit(player, max_health, max_health) - EventBus.shield_changed.emit(player, max_shield, max_shield) - EventBus.player_respawned.emit(player) + PlayerData.respawn() diff --git a/systems/role_system.gd b/systems/role_system.gd new file mode 100644 index 0000000..9b70c3c --- /dev/null +++ b/systems/role_system.gd @@ -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) diff --git a/systems/role_system.gd.uid b/systems/role_system.gd.uid new file mode 100644 index 0000000..ee53254 --- /dev/null +++ b/systems/role_system.gd.uid @@ -0,0 +1 @@ +uid://cuwueo5v43kap diff --git a/systems/shield_system.gd b/systems/shield_system.gd index 171a1ab..84e21e8 100644 --- a/systems/shield_system.gd +++ b/systems/shield_system.gd @@ -1,10 +1,28 @@ extends Node func _process(delta: float) -> void: - for entity in Stats.entities: + _process_player(delta) + _process_entities(delta, EnemyData.entities) + _process_entities(delta, BossData.entities) + +func _process_player(delta: float) -> void: + if not PlayerData.alive or PlayerData.max_shield <= 0: + return + if PlayerData.shield < PlayerData.max_shield: + PlayerData.shield_regen_timer += delta + if PlayerData.shield_regen_timer >= PlayerData.shield_regen_delay: + var regen_rate: float = PlayerData.max_shield / PlayerData.shield_regen_time + var shield: float = PlayerData.shield + regen_rate * delta + if shield >= PlayerData.max_shield: + shield = PlayerData.max_shield + EventBus.shield_regenerated.emit(get_tree().get_first_node_in_group("player")) + PlayerData.set_shield(shield) + +func _process_entities(delta: float, entities: Dictionary) -> void: + for entity in entities: if not is_instance_valid(entity): continue - var data: Dictionary = Stats.entities[entity] + var data: Dictionary = entities[entity] if not data["alive"]: continue var max_shield: float = data["max_shield"] @@ -22,17 +40,35 @@ func _process(delta: float) -> void: data["shield"] = shield EventBus.shield_changed.emit(entity, shield, max_shield) -func absorb(entity: Node, amount: float) -> float: - var shield: float = Stats.get_stat(entity, "shield") +func absorb(target: Node, amount: float) -> float: + var player: Node = get_tree().get_first_node_in_group("player") + if target == player: + if PlayerData.shield <= 0: + return amount + PlayerData.shield_regen_timer = 0.0 + var absorbed: float = min(amount, PlayerData.shield) + var shield: float = PlayerData.shield - absorbed + PlayerData.set_shield(shield) + if shield <= 0: + EventBus.shield_broken.emit(target) + return amount - absorbed + var data_source: Node = _get_data_source(target) + if not data_source: + return amount + var shield: float = data_source.get_stat(target, "shield") if shield == null or shield <= 0: return amount - Stats.set_stat(entity, "shield_regen_timer", 0.0) + data_source.set_stat(target, "shield_regen_timer", 0.0) var absorbed: float = min(amount, shield) shield -= absorbed - Stats.set_stat(entity, "shield", shield) - var max_shield: float = Stats.get_stat(entity, "max_shield") + data_source.set_shield(target, shield) if shield <= 0: - EventBus.shield_broken.emit(entity) - EventBus.shield_changed.emit(entity, shield, max_shield) + EventBus.shield_broken.emit(target) return amount - absorbed +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 diff --git a/systems/spawn_system.gd b/systems/spawn_system.gd index 0e244f3..cc4c6fe 100644 --- a/systems/spawn_system.gd +++ b/systems/spawn_system.gd @@ -2,8 +2,6 @@ extends Node const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn") -var portal_data: Dictionary = {} - func _ready() -> void: EventBus.health_changed.connect(_on_health_changed) EventBus.entity_died.connect(_on_entity_died) @@ -11,22 +9,18 @@ func _ready() -> void: func _on_health_changed(entity: Node, current: float, max_val: float) -> void: if not entity.is_in_group("portals"): return - if entity not in portal_data: - var base: BaseStats = Stats.get_base(entity) - var thresholds: Array[float] = base.thresholds if base is PortalStats else [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] - var triggered: Array[bool] = [] - triggered.resize(thresholds.size()) - triggered.fill(false) - portal_data[entity] = { "thresholds": thresholds, "triggered": triggered } if current <= 0: return - var data: Dictionary = portal_data[entity] + var data: Dictionary = PortalData.entities.get(entity, {}) + if data.is_empty(): + return var ratio: float = current / max_val - var base: BaseStats = Stats.get_base(entity) - var spawn_count: int = base.spawn_count if base is PortalStats else 3 - for i in range(data["thresholds"].size()): - if not data["triggered"][i] and ratio <= data["thresholds"][i]: - data["triggered"][i] = true + var thresholds: Array = data["thresholds"] + var triggered: Array = data["triggered"] + var spawn_count: int = data["spawn_count"] + for i in range(thresholds.size()): + if not triggered[i] and ratio <= thresholds[i]: + triggered[i] = true _spawn_enemies(entity, spawn_count) func _spawn_enemies(portal: Node, count: int) -> void: @@ -36,8 +30,6 @@ func _spawn_enemies(portal: Node, count: int) -> void: var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) portal.get_parent().add_child(entity) entity.global_position = portal.global_position + offset - entity.spawn_position = portal.global_position - entity.portal = portal spawned.append(entity) var player: Node = get_tree().get_first_node_in_group("player") if player: @@ -49,4 +41,4 @@ func _spawn_enemies(portal: Node, count: int) -> void: func _on_entity_died(entity: Node) -> void: if entity.is_in_group("portals"): - portal_data.erase(entity) + PortalData.deregister(entity) diff --git a/systems/targeting_system.gd b/systems/targeting_system.gd new file mode 100644 index 0000000..f2541cd --- /dev/null +++ b/systems/targeting_system.gd @@ -0,0 +1,66 @@ +extends Node + +func _ready() -> void: + EventBus.target_requested.connect(_on_target_requested) + EventBus.entity_died.connect(_on_entity_died) + EventBus.enemy_engaged.connect(_on_enemy_engaged) + EventBus.damage_dealt.connect(_on_damage_dealt) + +func _process(delta: float) -> void: + if PlayerData.in_combat: + PlayerData.combat_timer -= delta + if PlayerData.combat_timer <= 0: + PlayerData.in_combat = false + +func _on_target_requested(_player: Node, target: Node3D) -> void: + PlayerData.set_target(target) + +func _on_entity_died(entity: Node) -> void: + if entity == PlayerData.target: + PlayerData.set_target(null) + if PlayerData.in_combat: + _auto_target(entity) + +func _on_enemy_engaged(_enemy: Node, target: Node) -> void: + var player: Node = get_tree().get_first_node_in_group("player") + if target == player: + PlayerData.combat_timer = PlayerData.combat_timeout + PlayerData.in_combat = true + if PlayerData.target == null: + _auto_target() + +func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void: + var player: Node = get_tree().get_first_node_in_group("player") + if target == player: + PlayerData.combat_timer = PlayerData.combat_timeout + if not PlayerData.in_combat: + PlayerData.in_combat = true + if PlayerData.target == null: + _auto_target() + elif attacker == player: + PlayerData.in_combat = true + PlayerData.combat_timer = PlayerData.combat_timeout + +func _auto_target(exclude: Node = null) -> void: + var player: Node = get_tree().get_first_node_in_group("player") + if not player: + return + var nearest: Node3D = null + var nearest_dist: float = INF + for enemy in get_tree().get_nodes_in_group("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: + PlayerData.set_target(nearest) + return + for p in get_tree().get_nodes_in_group("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: + PlayerData.set_target(nearest) diff --git a/systems/targeting_system.gd.uid b/systems/targeting_system.gd.uid new file mode 100644 index 0000000..1e68347 --- /dev/null +++ b/systems/targeting_system.gd.uid @@ -0,0 +1 @@ +uid://bvjmdmof4vcyr