diff --git a/CLAUDE.md b/CLAUDE.md index e5888cd..28bebc0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,8 @@ Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. - `world/` — Hauptszene + portal_spawner - `healthbar.gd` — Shared Component - `systems/` — Spiellogik - - 9 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, buff) + - 11 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element) + - `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten) - `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config - `autoloads/` — Globaler Zustand - event_bus, game_state @@ -65,7 +66,7 @@ Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design - 5 Abilities pro Rolle: Single, AOE, Utility, Ult, Passive — jede Rolle hat eigene Werte - GCD 0.5s (gilt für 1, 2, 4), Utility ignoriert GCD - Heilung: Heiler heilt sich selbst (Singleplayer), is_heal Flag auf Ability -- Passive: Schadens-Boost (D), Schild-Boost (T), Heal-Boost (H) — je 50% +- Passive: Schadens-Boost (D), Schild-Boost (T), Heal-Boost (H) — je 50%, als Aura (50m Radius) - Targeting: Klick, TAB (cyclet "enemies" + "portals"), Auto-Target (Gegner > Portal) - Aggro-System: 1:1 Schaden, Tank 2x, Heilung 0.5x, verfällt -1/s, exponentiell außerhalb Portal-Radius @@ -75,6 +76,14 @@ Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design - Tank: Weniger Schaden, Nahkampf, Schild-Ult (300%), Schild-Passive - Heiler: Heilt statt schadet (Single, AOE, Ult), Heal-Passive, AOE macht Schaden +## Effekte & Elemente +- **EffectSystem**: Verwaltet Buffs, Debuffs, Auras auf allen Entities. Kein Stacking (gleicher Name → Refresh) +- **Effect Resource**: effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration (-1=permanent), is_multiplier, aura_radius, tick_interval, element +- **Auras**: Passive-Abilities werden als AURA-Effekte erstellt. Propagieren Buffs auf Spieler im aura_radius. Verlässt man Radius → Buff sofort weg +- **ElementSystem**: Verwaltet Elementar-Zustände auf Enemies + Portalen. Aktuell: Feuer (DoT 3/Tick, 2s Interval, 6s) +- **Abilities**: `element` Feld (0=NONE, 1=FIRE). Schadens-Abilities der Damage-Rolle haben element=1 (Feuer) +- **Effekt-Icons**: Auf Healthbars (Enemies, Boss, Portal) und Spieler-HUD. Aura=blau, Buff=grün, Debuff=rot + ## Szenenwechsel - Stats Autoload cached Spieler-Werte automatisch bei Szenenwechsel - GameState speichert Rolle + Position diff --git a/autoloads/event_bus.gd b/autoloads/event_bus.gd index 7d4003c..23d6698 100644 --- a/autoloads/event_bus.gd +++ b/autoloads/event_bus.gd @@ -2,15 +2,8 @@ extends Node # Intentionen (Input → System) signal ability_use_requested(player, ability_index) -signal auto_attack_tick(attacker) -signal target_requested(player, target) signal enemy_detected(enemy, player) -# Ergebnisse (System → Node) -signal combat_state_changed(player, in_combat) -signal enemy_state_changed(enemy, new_state) -signal enemy_target_changed(enemy, target) - # Kampf signal attack_executed(attacker, position, direction, damage) signal damage_dealt(attacker, target, damage) @@ -23,7 +16,6 @@ signal health_changed(entity, current, max_val) signal shield_changed(entity, current, max_val) signal shield_broken(entity) signal shield_regenerated(entity) -signal regeneration_changed(entity, current, max_val) # Spieler signal target_changed(player, target) @@ -45,3 +37,13 @@ signal portal_defeated(portal) # Dungeon signal dungeon_cleared() + +# Effects +signal effect_requested(target, effect, source) +signal effect_applied(target, effect) +signal effect_expired(target, effect) + +# Elements +signal element_damage_dealt(attacker, target, amount, element) +signal element_applied(target, element) +signal element_reaction(target, element_a, element_b, reaction_name) diff --git a/plan.md b/plan.md index 8d01767..8891933 100644 --- a/plan.md +++ b/plan.md @@ -17,7 +17,8 @@ scenes/ — Darstellung + Input world/ — Hauptszene + portal_spawner systems/ — Spiellogik aggro/ — AggroSystem (system, tracker, decay, events) + aggro_config - 9× *_system.gd — health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, buff + effect.gd — Effect Resource (Buff/Debuff/Aura Daten) + 10× *_system.gd — health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, effect, element autoloads/ — Globaler Zustand event_bus.gd game_state.gd @@ -26,7 +27,7 @@ autoloads/ — Globaler Zustand ## Szenenbaum - Welt - - Systems (10 Systeme als Child-Nodes) + - Systems (11 Systeme als Child-Nodes) - Taverne - Player - Portale (dynamisch) @@ -75,8 +76,14 @@ autoloads/ — Globaler Zustand - portal_defeated(portal) - Dungeon: - dungeon_cleared() -- Reserviert: - - auto_attack_tick, target_requested, combat_state_changed, enemy_state_changed, enemy_target_changed, regeneration_changed +- Effects: + - effect_requested(target, effect, source) + - effect_applied(target, effect) + - effect_expired(target, effect) +- Elements: + - element_damage_dealt(attacker, target, amount, element) + - element_applied(target, element) + - element_reaction(target, element_a, element_b, reaction_name) ## Stats (autoload/stats.gd) - Speichert aktuelle Attribute aller Entities @@ -101,12 +108,12 @@ autoloads/ — Globaler Zustand ## resources/roles/ ### Ability (ability.gd) -- ability_name, type, damage, ability_range, cooldown, uses_gcd, aoe_radius, icon, is_heal, passive_stat +- ability_name, type, damage, ability_range, cooldown, uses_gcd, aoe_radius, icon, is_heal, passive_stat, element - Typen: Single, AOE, Utility, Ult, Passive ### AbilitySet (ability_set.gd) - abilities (Array[Ability]), aa_damage, aa_range, aa_is_heal -### AbilityModifier (geplant) -- Verändert Ability (Element, Beruf, Prestige) +### AbilityModifier (geplant, Zukunft) +- Verändert Ability (Beruf, Prestige) ### Rollen-Ordner (damage/, tank/, healer/) - set.tres — AbilitySet der Rolle - abilities/ — 5 Ability .tres pro Rolle (single, aoe, utility, ult, passive) @@ -132,10 +139,27 @@ autoloads/ — Globaler Zustand - Ability-Ausführung (Single, AOE, Utility, Ult) + Auto-Attack in _process - Listener: ability_use_requested - Event: attack_executed, damage_requested, heal_requested -### BuffSystem (buff_system.gd) -- Passive-Buffs (damage/heal/shield Multiplikatoren in Stats) -- Listener: role_changed -- Event: buff_changed, shield_changed +### 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 +- Listener: role_changed, entity_died, effect_requested +- Event: buff_changed, shield_changed, effect_applied, effect_expired +### ElementSystem (element_system.gd) +- Verwaltet Element-Zustände auf Entities und löst Elementareffekte aus +- Element Enum: NONE, FIRE (erweiterbar) +- State: applied_elements Dictionary[Node, int] +- Nur Entities in Gruppen "enemies" oder "portals" erhalten Elemente +- Bei Element-Schaden: Element auf Ziel anwenden, passenden Effekt erstellen +- Feuer: DoT (3 Schaden/Tick, 2s Interval, 6s Dauer) +- Bei zwei verschiedenen Elementen: Reaktion auslösen (Zukunft) +- Listener: element_damage_dealt, entity_died, effect_expired +- Event: element_applied, element_reaction ### CooldownSystem (cooldown_system.gd) - Cooldown-Tracking, GCD, AA-Timer per Entity - register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem @@ -166,7 +190,7 @@ autoloads/ — Globaler Zustand ## Welt (world/) - world.tscn — Hauptszene (100x100m) - - Systems (alle 10 Systeme als Child-Nodes) + - Systems (alle 11 Systeme als Child-Nodes) - NavigationRegion3D - Boden (MeshInstance3D, 100x100m PlaneMesh) - Kollision (StaticBody3D, WorldBoundaryShape3D) @@ -185,7 +209,7 @@ autoloads/ — Globaler Zustand - Camera3D - Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats - Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested - - Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed + - Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready) - Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested - player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn - camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung @@ -199,7 +223,7 @@ 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 + - Healthbar (Sprite3D + SubViewport, healthbar.gd) — liest HP/Shield von Stats, zeigt Effekt-Icons - enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal - Aggro-Regeln (Werte in AggroConfig Resource): - Aufbau: @@ -242,7 +266,7 @@ autoloads/ — Globaler Zustand ## Dungeon (dungeon/) - dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht) - - Systems (alle 10 Systeme, temporär bis Welt parallel läuft) + - Systems (alle 11 Systeme, temporär bis Welt parallel läuft) - NavigationRegion3D - Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch) - Spieler (Instanz von player.tscn) @@ -257,9 +281,10 @@ autoloads/ — Globaler Zustand - hud.tscn — CanvasLayer - 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 +- hud.gd — Reagiert auf Events, liest Werte von Stats, zeigt Effekt-Icons # Abilities (Werte) - Schadens-Klasse: diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn index a349c1a..a24e84b 100644 --- a/scenes/dungeon/dungeon.tscn +++ b/scenes/dungeon/dungeon.tscn @@ -18,7 +18,8 @@ [ext_resource type="Script" path="res://systems/enemy_ai_system.gd" id="enemy_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/buff_system.gd" id="buff_system"] +[ext_resource type="Script" path="res://systems/effect_system.gd" id="effect_system"] +[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"] [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) @@ -90,8 +91,11 @@ script = ExtResource("respawn_system") [node name="SpawnSystem" type="Node" parent="Systems"] script = ExtResource("spawn_system") -[node name="BuffSystem" type="Node" parent="Systems"] -script = ExtResource("buff_system") +[node name="EffectSystem" type="Node" parent="Systems"] +script = ExtResource("effect_system") + +[node name="ElementSystem" type="Node" parent="Systems"] +script = ExtResource("element_system") [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] navigation_mesh = SubResource("NavigationMesh_1") diff --git a/scenes/healthbar.gd b/scenes/healthbar.gd index 17e6b93..a38a2b4 100644 --- a/scenes/healthbar.gd +++ b/scenes/healthbar.gd @@ -1,5 +1,9 @@ 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 @@ -8,19 +12,37 @@ extends Sprite3D 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: - texture = viewport.get_texture() 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: @@ -33,6 +55,7 @@ func _init_bars() -> void: 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") @@ -55,3 +78,82 @@ func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: 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/hud/hud.gd b/scenes/hud/hud.gd index 162839b..a02c209 100644 --- a/scenes/hud/hud.gd +++ b/scenes/hud/hud.gd @@ -17,9 +17,11 @@ const GCD_TIME := 0.5 ] 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) @@ -27,6 +29,8 @@ func _ready() -> void: EventBus.role_changed.connect(_on_role_changed) EventBus.respawn_tick.connect(_on_respawn_tick) EventBus.cooldown_tick.connect(_on_cooldown_tick) + EventBus.effect_applied.connect(_on_effect_applied) + EventBus.effect_expired.connect(_on_effect_expired) func _on_health_changed(entity: Node, current: float, max_val: float) -> void: if entity.name == "Player": @@ -74,3 +78,68 @@ func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) else: 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": + 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) + +func _on_effect_expired(target: Node, effect: Effect) -> void: + if target.name != "Player": + 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 + +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(2) + style.set_content_margin_all(2) + 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_color_override("font_color", Color.WHITE) + label.custom_minimum_size = Vector2(20, 20) + panel.add_child(label) + panel.custom_minimum_size = Vector2(24, 24) + panel.set_meta("effect_type", effect.type) + panel.set_meta("effect_name", effect.effect_name) + return panel diff --git a/scenes/player/role/ability.gd b/scenes/player/role/ability.gd index fff20d9..432c681 100644 --- a/scenes/player/role/ability.gd +++ b/scenes/player/role/ability.gd @@ -13,3 +13,4 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } @export var icon: String = "" @export var is_heal: bool = false @export var passive_stat: String = "damage" +@export var element: int = 0 diff --git a/scenes/player/role/damage/abilities/aoe.tres b/scenes/player/role/damage/abilities/aoe.tres index 860d19f..8cf6b23 100644 --- a/scenes/player/role/damage/abilities/aoe.tres +++ b/scenes/player/role/damage/abilities/aoe.tres @@ -10,3 +10,4 @@ damage = 20.0 ability_range = 5.0 cooldown = 3.0 icon = "2" +element = 1 diff --git a/scenes/player/role/damage/abilities/single.tres b/scenes/player/role/damage/abilities/single.tres index b1be495..921e429 100644 --- a/scenes/player/role/damage/abilities/single.tres +++ b/scenes/player/role/damage/abilities/single.tres @@ -9,3 +9,4 @@ damage = 30.0 ability_range = 20.0 cooldown = 2.0 icon = "1" +element = 1 diff --git a/scenes/player/role/damage/abilities/ult.tres b/scenes/player/role/damage/abilities/ult.tres index e5e0cc1..2be8c99 100644 --- a/scenes/player/role/damage/abilities/ult.tres +++ b/scenes/player/role/damage/abilities/ult.tres @@ -11,3 +11,4 @@ ability_range = 20.0 cooldown = 15.0 aoe_radius = 3.0 icon = "4" +element = 1 diff --git a/scenes/player/role/role.gd b/scenes/player/role/role.gd index e9c89f3..81f24dd 100644 --- a/scenes/player/role/role.gd +++ b/scenes/player/role/role.gd @@ -10,6 +10,9 @@ var current_role: int = Role.DAMAGE @onready var player: CharacterBody3D = get_parent() +func _ready() -> void: + set_role.call_deferred(current_role) + func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("class_tank"): set_role(Role.TANK) diff --git a/scenes/world/portal_spawner.gd b/scenes/world/portal_spawner.gd index 94e0095..7e13311 100644 --- a/scenes/world/portal_spawner.gd +++ b/scenes/world/portal_spawner.gd @@ -41,4 +41,8 @@ func _spawn_portal() -> void: portals.append(portal) func _cleanup_dead() -> void: - portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p)) + var valid: Array[Node] = [] + for p in portals: + if is_instance_valid(p): + valid.append(p) + portals = valid diff --git a/scenes/world/world.tscn b/scenes/world/world.tscn index 0f88624..0dceb79 100644 --- a/scenes/world/world.tscn +++ b/scenes/world/world.tscn @@ -5,9 +5,10 @@ [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://da2jm0awq2lnh" path="res://systems/buff_system.gd" id="buff_system"] [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="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] @@ -92,8 +93,11 @@ script = ExtResource("respawn_system") [node name="SpawnSystem" type="Node" parent="Systems" unique_id=1099032666] script = ExtResource("spawn_system") -[node name="BuffSystem" type="Node" parent="Systems" unique_id=1219368182] -script = ExtResource("buff_system") +[node name="EffectSystem" type="Node" parent="Systems" unique_id=1219368182] +script = ExtResource("effect_system") + +[node name="ElementSystem" type="Node" parent="Systems" unique_id=1401212832] +script = ExtResource("element_system") [node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506) diff --git a/systems/ability_system.gd b/systems/ability_system.gd index 6f89e6e..a80f2ad 100644 --- a/systems/ability_system.gd +++ b/systems/ability_system.gd @@ -34,8 +34,8 @@ func _try_auto_attack(player: Node) -> void: if dist > aa_range: return EventBus.damage_requested.emit(player, targeting.current_target, dmg) - var base: BaseStats = Stats.get_base(player) - var aa_cd: float = base.aa_cooldown if base is PlayerStats else 0.5 + 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: @@ -56,8 +56,8 @@ func _on_ability_use_requested(player: Node, ability_index: int) -> void: var success: bool = _execute_ability(player, ability) if not success: return - var base: BaseStats = Stats.get_base(player) - var gcd_time: float = base.gcd_time if base is PlayerStats else 0.5 + 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) @@ -100,6 +100,8 @@ func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float if not is_instance_valid(targeting.current_target): return false EventBus.damage_requested.emit(player, targeting.current_target, dmg) + if ability.element != 0: + EventBus.element_damage_dealt.emit(player, targeting.current_target, dmg, ability.element) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) return true @@ -120,6 +122,8 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: var dist: float = player.global_position.distance_to(enemy.global_position) if dist <= ability.ability_range: EventBus.damage_requested.emit(player, enemy, dmg) + if ability.element != 0: + EventBus.element_damage_dealt.emit(player, enemy, dmg, ability.element) hit = true if hit: EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) @@ -158,12 +162,16 @@ func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) - return false var target: Node3D = targeting.current_target EventBus.damage_requested.emit(player, target, dmg * 5.0) - var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range + 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: if enemy != target and is_instance_valid(enemy): var enemy_dist: float = target.global_position.distance_to(enemy.global_position) - if enemy_dist <= aoe_range: + if enemy_dist <= splash_range: EventBus.damage_requested.emit(player, enemy, dmg * 2.0) + if ability.element != 0: + EventBus.element_damage_dealt.emit(player, enemy, dmg * 2.0, ability.element) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0) return true diff --git a/systems/buff_system.gd b/systems/buff_system.gd deleted file mode 100644 index bbd7b57..0000000 --- a/systems/buff_system.gd +++ /dev/null @@ -1,37 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.role_changed.connect(_on_role_changed) - -func _on_role_changed(player: Node, _role_type: int) -> void: - var role: Node = player.get_node_or_null("Role") - if not role: - return - var ability_set: AbilitySet = role.get_ability_set() - if not ability_set: - return - var damage_mult := 1.0 - var heal_mult := 1.0 - var shield_mult := 1.0 - for ability in ability_set.abilities: - if ability and ability.type == Ability.Type.PASSIVE: - var bonus: float = ability.damage / 100.0 - match ability.passive_stat: - "damage": - damage_mult = 1.0 + bonus - "heal": - heal_mult = 1.0 + bonus - "shield": - shield_mult = 1.0 + bonus - Stats.set_stat(player, "buff_damage", damage_mult) - Stats.set_stat(player, "buff_heal", heal_mult) - Stats.set_stat(player, "buff_shield", shield_mult) - var base: BaseStats = Stats.get_base(player) - if base: - var new_max: float = base.max_shield * shield_mult - Stats.set_stat(player, "max_shield", new_max) - var shield: float = Stats.get_stat(player, "shield") - shield = min(shield, new_max) - Stats.set_stat(player, "shield", shield) - EventBus.shield_changed.emit(player, shield, new_max) - EventBus.buff_changed.emit(player, "damage", damage_mult) diff --git a/systems/buff_system.gd.uid b/systems/buff_system.gd.uid deleted file mode 100644 index 742a992..0000000 --- a/systems/buff_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://da2jm0awq2lnh diff --git a/systems/effect.gd b/systems/effect.gd new file mode 100644 index 0000000..7b91f89 --- /dev/null +++ b/systems/effect.gd @@ -0,0 +1,14 @@ +extends Resource +class_name Effect + +enum Type { BUFF, DEBUFF, AURA } + +@export var effect_name: String = "" +@export var type: Type = Type.BUFF +@export var stat: String = "" +@export var value: float = 0.0 +@export var duration: float = -1.0 +@export var is_multiplier: bool = true +@export var aura_radius: float = 0.0 +@export var tick_interval: float = 0.0 +@export var element: int = 0 diff --git a/systems/effect.gd.uid b/systems/effect.gd.uid new file mode 100644 index 0000000..6897369 --- /dev/null +++ b/systems/effect.gd.uid @@ -0,0 +1 @@ +uid://djbni7iy5pw2m diff --git a/systems/effect_system.gd b/systems/effect_system.gd new file mode 100644 index 0000000..d60ae1b --- /dev/null +++ b/systems/effect_system.gd @@ -0,0 +1,190 @@ +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 new file mode 100644 index 0000000..f4afc65 --- /dev/null +++ b/systems/effect_system.gd.uid @@ -0,0 +1 @@ +uid://drdlh6tq0dfwo diff --git a/systems/element_system.gd b/systems/element_system.gd new file mode 100644 index 0000000..e4fba33 --- /dev/null +++ b/systems/element_system.gd @@ -0,0 +1,53 @@ +extends Node + +enum Element { NONE, FIRE } + +var applied_elements: Dictionary = {} + +func _ready() -> void: + EventBus.element_damage_dealt.connect(_on_element_damage_dealt) + EventBus.entity_died.connect(_on_entity_died) + EventBus.effect_expired.connect(_on_effect_expired) + +func _on_element_damage_dealt(attacker: Node, target: Node, _amount: float, element: int) -> void: + if element == Element.NONE: + return + if not target.is_in_group("enemies") and not target.is_in_group("portals"): + return + var current: int = applied_elements.get(target, Element.NONE) + if current != Element.NONE and current != element: + _trigger_reaction(attacker, target, current, element) + return + _apply_element(attacker, target, element) + +func _apply_element(source: Node, target: Node, element: int) -> void: + applied_elements[target] = element + EventBus.element_applied.emit(target, element) + match element: + Element.FIRE: + _apply_fire(source, target) + +func _apply_fire(source: Node, target: Node) -> void: + var fire_dot := Effect.new() + fire_dot.effect_name = "Burning" + fire_dot.type = Effect.Type.DEBUFF + fire_dot.stat = "damage" + fire_dot.value = 3.0 + fire_dot.duration = 6.0 + fire_dot.is_multiplier = false + fire_dot.tick_interval = 2.0 + fire_dot.element = Element.FIRE + EventBus.effect_requested.emit(target, fire_dot, source) + +func _trigger_reaction(_attacker: Node, target: Node, _elem_a: int, _elem_b: int) -> void: + applied_elements.erase(target) + EventBus.element_reaction.emit(target, _elem_a, _elem_b, "") + +func _on_entity_died(entity: Node) -> void: + applied_elements.erase(entity) + +func _on_effect_expired(target: Node, effect: Effect) -> void: + if effect.element != Element.NONE: + var current: int = applied_elements.get(target, Element.NONE) + if current == effect.element: + applied_elements.erase(target) diff --git a/systems/element_system.gd.uid b/systems/element_system.gd.uid new file mode 100644 index 0000000..e6bfc53 --- /dev/null +++ b/systems/element_system.gd.uid @@ -0,0 +1 @@ +uid://bqebxfvticxto