update
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
38
plan.md
38
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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
8
resources/stats/aggro_config.gd
Normal file
8
resources/stats/aggro_config.gd
Normal file
@@ -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
|
||||
1
resources/stats/aggro_config.gd.uid
Normal file
1
resources/stats/aggro_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3gwl1wweld2x
|
||||
6
resources/stats/aggro_config.tres
Normal file
6
resources/stats/aggro_config.tres
Normal file
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
68
systems/aggro/aggro_decay.gd
Normal file
68
systems/aggro/aggro_decay.gd
Normal file
@@ -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)
|
||||
1
systems/aggro/aggro_decay.gd.uid
Normal file
1
systems/aggro/aggro_decay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cysg30lud2ta2
|
||||
52
systems/aggro/aggro_events.gd
Normal file
52
systems/aggro/aggro_events.gd
Normal file
@@ -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)
|
||||
1
systems/aggro/aggro_events.gd.uid
Normal file
1
systems/aggro/aggro_events.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cyffo1g4uhmwh
|
||||
17
systems/aggro/aggro_system.gd
Normal file
17
systems/aggro/aggro_system.gd
Normal file
@@ -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)
|
||||
82
systems/aggro/aggro_tracker.gd
Normal file
82
systems/aggro/aggro_tracker.gd
Normal file
@@ -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)
|
||||
1
systems/aggro/aggro_tracker.gd.uid
Normal file
1
systems/aggro/aggro_tracker.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c7gsu2qddsor6
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user