From c7e6f8f4b52de47ec39cb60045ea6936d345129b Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Thu, 2 Apr 2026 18:33:53 +0200 Subject: [PATCH] update --- CLAUDE.md | 5 +- autoload/event_bus.gd | 1 + dungeon/dungeon.tscn | 14 ++- enemy/enemy.gd | 5 +- enemy/enemy.tscn | 2 +- plan.md | 38 ++++--- portal/portal.gd | 2 +- resources/roles/damage/set.tres | 2 +- resources/roles/healer/set.tres | 2 +- resources/roles/tank/set.tres | 2 +- resources/stats/aggro_config.gd | 8 ++ resources/stats/aggro_config.gd.uid | 1 + resources/stats/aggro_config.tres | 6 ++ resources/stats/boss_stats.tres | 2 +- resources/stats/enemy_stats.gd | 2 +- systems/aggro/aggro_decay.gd | 68 +++++++++++++ systems/aggro/aggro_decay.gd.uid | 1 + systems/aggro/aggro_events.gd | 52 ++++++++++ systems/aggro/aggro_events.gd.uid | 1 + systems/aggro/aggro_system.gd | 17 ++++ systems/{ => aggro}/aggro_system.gd.uid | 0 systems/aggro/aggro_tracker.gd | 82 +++++++++++++++ systems/aggro/aggro_tracker.gd.uid | 1 + systems/aggro_system.gd | 130 ------------------------ world/world.tscn | 14 ++- 25 files changed, 301 insertions(+), 157 deletions(-) create mode 100644 resources/stats/aggro_config.gd create mode 100644 resources/stats/aggro_config.gd.uid create mode 100644 resources/stats/aggro_config.tres create mode 100644 systems/aggro/aggro_decay.gd create mode 100644 systems/aggro/aggro_decay.gd.uid create mode 100644 systems/aggro/aggro_events.gd create mode 100644 systems/aggro/aggro_events.gd.uid create mode 100644 systems/aggro/aggro_system.gd rename systems/{ => aggro}/aggro_system.gd.uid (100%) create mode 100644 systems/aggro/aggro_tracker.gd create mode 100644 systems/aggro/aggro_tracker.gd.uid delete mode 100644 systems/aggro_system.gd diff --git a/CLAUDE.md b/CLAUDE.md index 7a97f5a..cbbc3b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,10 +30,11 @@ Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare n - `dungeon/` — Dungeon (.tscn + .gd): dungeon, dungeon_manager - `hud/` — HUD (.tscn + .gd): hud - `world/` — Hauptszene (.tscn + .gd): world, portal_spawner -- `systems/` — 10 Systeme: health, shield, damage, ability, cooldown, aggro, enemy_ai, respawn, spawn, buff +- `systems/` — 9 Systeme (health, shield, damage, ability, cooldown, enemy_ai, respawn, spawn, buff) + - `aggro/` — AggroSystem (aggro_system, aggro_tracker, aggro_decay, aggro_events) - `autoload/` — EventBus (event_bus), Stats (stats), GameState (game_state) - `components/` — Shared Components: healthbar -- `resources/stats/` — Stats-Klassen (.gd) + Daten (.tres): base_stats, player_stats, enemy_stats, boss_stats, portal_stats +- `resources/stats/` — Stats-Klassen (.gd) + Daten (.tres): base_stats, player_stats, enemy_stats, boss_stats, portal_stats, aggro_config - `resources/roles/` — Ability/AbilitySet-Klassen (.gd) + pro Rolle (damage, tank, healer): - `{rolle}/set.tres` — AbilitySet der Rolle - `{rolle}/abilities/` — Abilities (single, aoe, utility, ult, passive) diff --git a/autoload/event_bus.gd b/autoload/event_bus.gd index c0bcb86..7d4003c 100644 --- a/autoload/event_bus.gd +++ b/autoload/event_bus.gd @@ -37,6 +37,7 @@ 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/dungeon/dungeon.tscn b/dungeon/dungeon.tscn index 26cc1ff..1a1ed73 100644 --- a/dungeon/dungeon.tscn +++ b/dungeon/dungeon.tscn @@ -11,7 +11,10 @@ [ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] [ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"] [ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"] -[ext_resource type="Script" path="res://systems/aggro_system.gd" id="aggro_system"] +[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] +[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] +[ext_resource type="Script" path="res://systems/aggro/aggro_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/respawn_system.gd" id="respawn_system"] [ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] @@ -69,6 +72,15 @@ script = ExtResource("cooldown_system") [node name="AggroSystem" type="Node" parent="Systems"] script = ExtResource("aggro_system") +[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"] +script = ExtResource("aggro_tracker") + +[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"] +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") diff --git a/enemy/enemy.gd b/enemy/enemy.gd index f97e5ba..7f57316 100644 --- a/enemy/enemy.gd +++ b/enemy/enemy.gd @@ -32,5 +32,6 @@ 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: - pass +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/enemy/enemy.tscn b/enemy/enemy.tscn index 8738477..6fde9cc 100644 --- a/enemy/enemy.tscn +++ b/enemy/enemy.tscn @@ -34,7 +34,7 @@ height = 1.0 material = SubResource("StandardMaterial3D_1") [sub_resource type="SphereShape3D" id="SphereShape3D_1"] -radius = 8.0 +radius = 10.0 [node name="Enemy" type="CharacterBody3D"] script = ExtResource("1") diff --git a/plan.md b/plan.md index fac52a6..f1baa1c 100644 --- a/plan.md +++ b/plan.md @@ -8,11 +8,12 @@ portal/ — Portal + Gate (.tscn + .gd) dungeon/ — Dungeon (.tscn + .gd) hud/ — HUD (.tscn + .gd) world/ — Hauptszene (.tscn + .gd) -systems/ — 10 Gameplay-Systeme (.gd) +systems/ — 9 Gameplay-Systeme (.gd) + aggro/ — AggroSystem (aggro_system, aggro_tracker, aggro_decay, aggro_events) autoload/ — EventBus, Stats, GameState (.gd) components/ — Shared UI: healthbar (.gd) resources/ - stats/ — Stats-Klassen (.gd) + Daten (.tres) + stats/ — Stats-Klassen (.gd) + Daten (.tres) + AggroConfig roles/ — Ability/AbilitySet-Klassen (.gd) + Rollen-Daten damage/ — set.tres + abilities/ tank/ — set.tres + abilities/ @@ -137,9 +138,12 @@ resources/ - Event: cooldown_tick ### DamageSystem (damage_system.gd) - Reserviert für spätere Schadensberechnung (aktuell leer) -### AggroSystem (aggro_system.gd) -- Aggro-Tabellen, Decay, Zielwahl, Nearby-Alerting -- Listener: damage_dealt, heal_requested, entity_died, enemy_detected +### AggroSystem (systems/aggro/) +- Systemweite Werte in AggroConfig Resource (resources/stats/aggro_config.tres) +- aggro_system.gd — Parent, Config halten, Children verdrahten +- aggro_tracker.gd — Aggro-Tabellen, Players-in-Range, Zielwahl, Radius-Helper +- aggro_decay.gd — Combat-Timer, Decay-Berechnung, Spread, Alert +- aggro_events.gd — Signal-Handler (damage_dealt, heal_requested, entity_died, enemy_detected, enemy_lost) - Event: enemy_engaged ### EnemyAISystem (enemy_ai_system.gd) - ATTACK-State: Range-Check, Timer, Schaden @@ -193,15 +197,21 @@ resources/ - EnemyMovement (Node, enemy_movement.gd) — Empfängt Bewegungsbefehle - Healthbar (Sprite3D + SubViewport, healthbar.gd) — liest HP/Shield von Stats - enemy.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal -- Aggro-Regeln: - - Schaden = Aggro (1:1) - - Heilung = Aggro (0.5x) - - Tank = Aggro-Multiplikator (2x) - - Aggro verfällt -1/s - - Spieler im Portal-Radius: Aggro bleibt bei mindestens 1 - - Außerhalb Portal-Radius: Aggro verfällt exponentiell (1%, 2%, 4%, 8%, ...) - - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert - - Bei Spieler-Tod → Aggro auf 0 +- Aggro-Regeln (Werte in AggroConfig Resource): + - Aufbau: + - Schaden = Aggro (1:1), Tank 2x Multiplikator + - Heilung = 0.5x Aggro auf alle Gegner die Heiler kennen + - Aggro-Spread: 50% des Aggro an Gegner im alert_radius (10m) + - Detection-Area (10m): +1 Aggro, Alert an Nachbarn im alert_radius + - Kampfstatus: + - Spieler in DetectionArea → immer im Kampf (kein Decay) + - Spieler verlässt DetectionArea → 5s Combat-Timeout, dann Decay + - Schaden verursachen setzt Combat-Timer zurück + - Abbau (nach Combat-Timeout): + - Basis: -aggro_decay/s (default 1.0) + - Exponentieller Decay basierend auf Zeit seit Kampfende (1%·2^sekunden) + - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert + - Bei Spieler-Tod → Aggro auf 0 ## Boss (enemy/) - boss.tscn — wie enemy.tscn aber größer (Mesh lila, 1.5x) diff --git a/portal/portal.gd b/portal/portal.gd index 419e34b..4505000 100644 --- a/portal/portal.gd +++ b/portal/portal.gd @@ -19,8 +19,8 @@ func _on_entity_died(entity: Node) -> void: return var pos: Vector3 = global_position var gate: Node3D = GATE_SCENE.instantiate() - gate.global_position = pos 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): diff --git a/resources/roles/damage/set.tres b/resources/roles/damage/set.tres index ae7df7c..4af239e 100644 --- a/resources/roles/damage/set.tres +++ b/resources/roles/damage/set.tres @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://resources/roles/ability.gd" id="1_ability"] [ext_resource type="Resource" uid="uid://dwvc8b3cmce8l" path="res://resources/roles/damage/abilities/single.tres" id="2"] [ext_resource type="Resource" uid="uid://bpx3l13iuynfv" path="res://resources/roles/damage/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/roles/damage/abilities/utility.tres" id="4"] +[ext_resource type="Resource" path="res://resources/roles/damage/abilities/utility.tres" id="4"] [ext_resource type="Resource" uid="uid://s32wvlww2ls2" path="res://resources/roles/damage/abilities/ult.tres" id="5"] [ext_resource type="Resource" uid="uid://dadpl32yujwhe" path="res://resources/roles/damage/abilities/passive.tres" id="6"] diff --git a/resources/roles/healer/set.tres b/resources/roles/healer/set.tres index 8ca64c5..c523517 100644 --- a/resources/roles/healer/set.tres +++ b/resources/roles/healer/set.tres @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://resources/roles/ability.gd" id="1_ability"] [ext_resource type="Resource" path="res://resources/roles/healer/abilities/single.tres" id="2"] [ext_resource type="Resource" path="res://resources/roles/healer/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/roles/healer/abilities/utility.tres" id="4"] +[ext_resource type="Resource" path="res://resources/roles/healer/abilities/utility.tres" id="4"] [ext_resource type="Resource" path="res://resources/roles/healer/abilities/ult.tres" id="5"] [ext_resource type="Resource" path="res://resources/roles/healer/abilities/passive.tres" id="6"] diff --git a/resources/roles/tank/set.tres b/resources/roles/tank/set.tres index 7962261..e56d5b8 100644 --- a/resources/roles/tank/set.tres +++ b/resources/roles/tank/set.tres @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://resources/roles/ability.gd" id="1_ability"] [ext_resource type="Resource" path="res://resources/roles/tank/abilities/single.tres" id="2"] [ext_resource type="Resource" path="res://resources/roles/tank/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" uid="uid://du0hyuuj26ea0" path="res://resources/roles/tank/abilities/utility.tres" id="4"] +[ext_resource type="Resource" path="res://resources/roles/tank/abilities/utility.tres" id="4"] [ext_resource type="Resource" path="res://resources/roles/tank/abilities/ult.tres" id="5"] [ext_resource type="Resource" path="res://resources/roles/tank/abilities/passive.tres" id="6"] diff --git a/resources/stats/aggro_config.gd b/resources/stats/aggro_config.gd new file mode 100644 index 0000000..bb6ad14 --- /dev/null +++ b/resources/stats/aggro_config.gd @@ -0,0 +1,8 @@ +extends Resource +class_name AggroConfig + +@export var combat_timeout := 5.0 +@export var tank_multiplier := 2.0 +@export var heal_multiplier := 0.5 +@export var spread_multiplier := 0.5 +@export var exponential_decay_factor := 0.01 diff --git a/resources/stats/aggro_config.gd.uid b/resources/stats/aggro_config.gd.uid new file mode 100644 index 0000000..7892d27 --- /dev/null +++ b/resources/stats/aggro_config.gd.uid @@ -0,0 +1 @@ +uid://b3gwl1wweld2x diff --git a/resources/stats/aggro_config.tres b/resources/stats/aggro_config.tres new file mode 100644 index 0000000..c67fa68 --- /dev/null +++ b/resources/stats/aggro_config.tres @@ -0,0 +1,6 @@ +[gd_resource type="Resource" script_class="AggroConfig" format=3] + +[ext_resource type="Script" path="res://resources/stats/aggro_config.gd" id="1"] + +[resource] +script = ExtResource("1") diff --git a/resources/stats/boss_stats.tres b/resources/stats/boss_stats.tres index 3bc6604..188c934 100644 --- a/resources/stats/boss_stats.tres +++ b/resources/stats/boss_stats.tres @@ -17,4 +17,4 @@ regen_fast = 0.1 regen_slow = 0.01 aggro_decay = 1.0 portal_radius = 10.0 -alert_radius = 3.0 +alert_radius = 10.0 diff --git a/resources/stats/enemy_stats.gd b/resources/stats/enemy_stats.gd index d8cfd3e..439cf90 100644 --- a/resources/stats/enemy_stats.gd +++ b/resources/stats/enemy_stats.gd @@ -9,4 +9,4 @@ class_name EnemyStats @export var regen_slow := 0.01 @export var aggro_decay := 1.0 @export var portal_radius := 10.0 -@export var alert_radius := 3.0 +@export var alert_radius := 10.0 diff --git a/systems/aggro/aggro_decay.gd b/systems/aggro/aggro_decay.gd new file mode 100644 index 0000000..3475cdc --- /dev/null +++ b/systems/aggro/aggro_decay.gd @@ -0,0 +1,68 @@ +extends Node + +var tracker: Node +var config: AggroConfig +var last_damage_time: Dictionary = {} + +func process(delta: float) -> void: + _update_combat_timers(delta) + for enemy in tracker.aggro_tables.keys(): + if not is_instance_valid(enemy): + tracker.aggro_tables.erase(enemy) + tracker.players_in_range.erase(enemy) + continue + _decay_aggro(enemy, delta) + tracker.update_target(enemy) + +func _update_combat_timers(delta: float) -> void: + for player in last_damage_time.keys(): + if not is_instance_valid(player): + last_damage_time.erase(player) + else: + last_damage_time[player] += delta + +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 + for player in table.keys(): + if is_in_combat(player): + continue + var time_since_combat: float = last_damage_time.get(player, config.combat_timeout) - config.combat_timeout + var decay: float = aggro_decay * delta + decay += _exponential_decay(table[player], time_since_combat, delta) + table[player] -= decay + if table[player] <= 0: + table.erase(player) + +func reset_combat_timer(player: Node) -> void: + last_damage_time[player] = 0.0 + +func is_in_combat(player: Node) -> bool: + if tracker.is_player_in_any_range(player): + return true + return last_damage_time.get(player, config.combat_timeout + 1.0) < config.combat_timeout + +func _exponential_decay(aggro: float, time_outside: float, delta: float) -> float: + if time_outside <= 0: + return 0.0 + return aggro * config.exponential_decay_factor * pow(2, time_outside) * delta + +func spread_aggro(source: Node, attacker: Node, amount: float) -> void: + if not is_instance_valid(source): + return + var radius: float = tracker.get_alert_radius(source) + for enemy in tracker.get_enemies_in_radius(source, radius): + tracker.add_aggro(enemy, attacker, amount) + +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: + tracker.add_aggro(other, target, 1.0) + other.target = target + other.state = other.State.CHASE + EventBus.enemy_engaged.emit(other, target) + +func erase_entity(entity: Node) -> void: + last_damage_time.erase(entity) diff --git a/systems/aggro/aggro_decay.gd.uid b/systems/aggro/aggro_decay.gd.uid new file mode 100644 index 0000000..90e41dd --- /dev/null +++ b/systems/aggro/aggro_decay.gd.uid @@ -0,0 +1 @@ +uid://cysg30lud2ta2 diff --git a/systems/aggro/aggro_events.gd b/systems/aggro/aggro_events.gd new file mode 100644 index 0000000..a6b573a --- /dev/null +++ b/systems/aggro/aggro_events.gd @@ -0,0 +1,52 @@ +extends Node + +var tracker: Node +var decay: Node +var config: AggroConfig + +func _ready() -> void: + EventBus.damage_dealt.connect(_on_damage_dealt) + EventBus.heal_requested.connect(_on_heal_requested) + EventBus.entity_died.connect(_on_entity_died) + EventBus.enemy_detected.connect(_on_enemy_detected) + EventBus.enemy_lost.connect(_on_enemy_lost) + +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: + 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 + EventBus.enemy_engaged.emit(enemy, player) + decay.alert_nearby(enemy, player) + +func _on_enemy_lost(enemy: Node, player: Node) -> void: + tracker.remove_player_in_range(enemy, player) + +func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: + if not target.is_in_group("enemies") and not target.is_in_group("portals"): + 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: + multiplier = config.tank_multiplier + var aggro: float = amount * multiplier + tracker.add_aggro(target, attacker, aggro) + decay.spread_aggro(target, attacker, aggro * config.spread_multiplier) + +func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void: + if not healer.is_in_group("player"): + return + for enemy in tracker.aggro_tables: + if is_instance_valid(enemy) and healer in tracker.aggro_tables[enemy]: + tracker.add_aggro(enemy, healer, amount * config.heal_multiplier) + +func _on_entity_died(entity: Node) -> void: + tracker.erase_entity(entity) + decay.erase_entity(entity) diff --git a/systems/aggro/aggro_events.gd.uid b/systems/aggro/aggro_events.gd.uid new file mode 100644 index 0000000..9f4f901 --- /dev/null +++ b/systems/aggro/aggro_events.gd.uid @@ -0,0 +1 @@ +uid://cyffo1g4uhmwh diff --git a/systems/aggro/aggro_system.gd b/systems/aggro/aggro_system.gd new file mode 100644 index 0000000..c37e181 --- /dev/null +++ b/systems/aggro/aggro_system.gd @@ -0,0 +1,17 @@ +extends Node + +@export var config: AggroConfig = preload("res://resources/stats/aggro_config.tres") + +@onready var tracker: Node = $AggroTracker +@onready var decay: Node = $AggroDecay +@onready var events: Node = $AggroEvents + +func _ready() -> void: + decay.tracker = tracker + decay.config = config + events.tracker = tracker + events.decay = decay + events.config = config + +func _process(delta: float) -> void: + decay.process(delta) diff --git a/systems/aggro_system.gd.uid b/systems/aggro/aggro_system.gd.uid similarity index 100% rename from systems/aggro_system.gd.uid rename to systems/aggro/aggro_system.gd.uid diff --git a/systems/aggro/aggro_tracker.gd b/systems/aggro/aggro_tracker.gd new file mode 100644 index 0000000..5dc4734 --- /dev/null +++ b/systems/aggro/aggro_tracker.gd @@ -0,0 +1,82 @@ +extends Node + +var aggro_tables: Dictionary = {} +var players_in_range: Dictionary = {} + +func add_aggro(enemy: Node, player: Node, amount: float) -> void: + if enemy not in aggro_tables: + aggro_tables[enemy] = {} + if player in aggro_tables[enemy]: + aggro_tables[enemy][player] += amount + else: + aggro_tables[enemy][player] = amount + +func remove_aggro(enemy: Node, player: Node, amount: float) -> void: + if enemy in aggro_tables and player in aggro_tables[enemy]: + aggro_tables[enemy][player] -= amount + if aggro_tables[enemy][player] <= 0: + aggro_tables[enemy].erase(player) + +func add_player_in_range(enemy: Node, player: Node) -> void: + if enemy not in players_in_range: + players_in_range[enemy] = [] + if player not in players_in_range[enemy]: + players_in_range[enemy].append(player) + +func remove_player_in_range(enemy: Node, player: Node) -> void: + if enemy in players_in_range: + players_in_range[enemy].erase(player) + +func is_player_in_any_range(player: Node) -> bool: + for enemy in players_in_range: + if is_instance_valid(enemy) and player in players_in_range[enemy]: + return true + return false + +func get_top_target(table: Dictionary) -> Node: + var top: Node = null + var top_val := 0.0 + for player in table: + if is_instance_valid(player) and table[player] > top_val: + top_val = table[player] + top = player + 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 + +func get_enemies_in_radius(source: Node, radius: float) -> Array: + var result: Array = [] + for enemy in get_tree().get_nodes_in_group("enemies"): + if enemy != source and is_instance_valid(enemy): + var dist: float = source.global_position.distance_to(enemy.global_position) + if dist <= radius: + result.append(enemy) + 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 + +func erase_entity(entity: Node) -> void: + aggro_tables.erase(entity) + players_in_range.erase(entity) + 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 + for enemy in players_in_range: + if is_instance_valid(enemy): + players_in_range[enemy].erase(entity) diff --git a/systems/aggro/aggro_tracker.gd.uid b/systems/aggro/aggro_tracker.gd.uid new file mode 100644 index 0000000..6b96555 --- /dev/null +++ b/systems/aggro/aggro_tracker.gd.uid @@ -0,0 +1 @@ +uid://c7gsu2qddsor6 diff --git a/systems/aggro_system.gd b/systems/aggro_system.gd deleted file mode 100644 index 51e2ec7..0000000 --- a/systems/aggro_system.gd +++ /dev/null @@ -1,130 +0,0 @@ -extends Node - -var aggro_tables: Dictionary = {} -var seconds_outside: Dictionary = {} - -func _ready() -> void: - EventBus.damage_dealt.connect(_on_damage_dealt) - EventBus.heal_requested.connect(_on_heal_requested) - EventBus.entity_died.connect(_on_entity_died) - EventBus.enemy_detected.connect(_on_enemy_detected) - -func _process(delta: float) -> void: - for enemy in aggro_tables.keys(): - if not is_instance_valid(enemy): - aggro_tables.erase(enemy) - seconds_outside.erase(enemy) - continue - _decay_aggro(enemy, delta) - _update_target(enemy) - -func _decay_aggro(enemy: Node, delta: float) -> void: - var table: Dictionary = aggro_tables[enemy] - var base: BaseStats = Stats.get_base(enemy) - var portal_radius: float = base.portal_radius if base is EnemyStats else 10.0 - var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0 - - var outside_portal := false - if "portal" in enemy and enemy.portal and is_instance_valid(enemy.portal): - var dist: float = enemy.global_position.distance_to(enemy.portal.global_position) - if dist > portal_radius: - outside_portal = true - seconds_outside[enemy] = seconds_outside.get(enemy, 0.0) + delta - else: - seconds_outside[enemy] = 0.0 - - for player in table.keys(): - var decay: float = aggro_decay * delta - if outside_portal: - var bonus: float = table[player] * 0.01 * pow(2, seconds_outside.get(enemy, 0.0)) * delta - decay += bonus - table[player] -= decay - if not outside_portal and "portal" in enemy and enemy.portal and is_instance_valid(player): - var player_dist: float = player.global_position.distance_to(enemy.portal.global_position) - if player_dist <= portal_radius and table[player] < 1.0: - table[player] = 1.0 - if table[player] <= 0: - table.erase(player) - -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 - -func _add_aggro(enemy: Node, player: Node, amount: float) -> void: - if enemy not in aggro_tables: - aggro_tables[enemy] = {} - if player in aggro_tables[enemy]: - aggro_tables[enemy][player] += amount - else: - aggro_tables[enemy][player] = amount - -func _get_top_target(table: Dictionary) -> Node: - var top: Node = null - var top_val := 0.0 - for player in table: - if is_instance_valid(player) and table[player] > top_val: - top_val = table[player] - top = player - return top - -func _alert_nearby(enemy: Node, target: Node) -> void: - var base: BaseStats = Stats.get_base(enemy) - var alert_radius: float = base.alert_radius if base is EnemyStats else 3.0 - var enemies := enemy.get_tree().get_nodes_in_group("enemies") - for other in enemies: - if other != enemy and is_instance_valid(other) and "state" in other: - if other.state == other.State.IDLE: - var dist: float = enemy.global_position.distance_to(other.global_position) - if dist <= alert_radius: - _add_aggro(other, target, 1.0) - other.target = target - other.state = other.State.CHASE - EventBus.enemy_engaged.emit(other, target) - -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: - return - _add_aggro(enemy, player, 1.0) - if "state" in enemy: - enemy.target = player - enemy.state = enemy.State.CHASE - EventBus.enemy_engaged.emit(enemy, player) - _alert_nearby(enemy, player) - -func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: - if not target.is_in_group("enemies") and not target.is_in_group("portals"): - return - var multiplier := 1.0 - var role: Node = attacker.get_node_or_null("Role") - if role and role.current_role == 0: - multiplier = 2.0 - _add_aggro(target, attacker, amount * multiplier) - -func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void: - if not healer.is_in_group("player"): - return - for enemy in aggro_tables: - if is_instance_valid(enemy) and healer in aggro_tables[enemy]: - _add_aggro(enemy, healer, amount * 0.5) - -func _on_entity_died(entity: Node) -> void: - aggro_tables.erase(entity) - seconds_outside.erase(entity) - 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 diff --git a/world/world.tscn b/world/world.tscn index fd5b56d..d43d2fe 100644 --- a/world/world.tscn +++ b/world/world.tscn @@ -1,7 +1,10 @@ [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://cm7ehl2pexcst" path="res://systems/aggro_system.gd" id="aggro_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://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"] @@ -71,6 +74,15 @@ script = ExtResource("cooldown_system") [node name="AggroSystem" type="Node" parent="Systems" unique_id=1539448343] script = ExtResource("aggro_system") +[node name="AggroTracker" type="Node" parent="Systems/AggroSystem" unique_id=1597893665] +script = ExtResource("aggro_tracker") + +[node name="AggroDecay" type="Node" parent="Systems/AggroSystem" unique_id=1571705506] +script = ExtResource("aggro_decay") + +[node name="AggroEvents" type="Node" parent="Systems/AggroSystem" unique_id=1936723580] +script = ExtResource("aggro_events") + [node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042] script = ExtResource("enemy_ai_system")