From e76c66eda61aeb591c5fd50565dbc409d013e5c8 Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Wed, 1 Apr 2026 22:53:28 +0200 Subject: [PATCH] update --- CLAUDE.md | 50 +++--- plan.md | 74 ++++---- project.godot | 1 + resources/stats/boss_stats.tres | 13 +- resources/stats/enemy_stats.tres | 8 +- resources/stats/player_stats.tres | 7 +- resources/stats/portal_stats.tres | 6 +- scenes/dungeon/dungeon.tscn | 42 +++++ scenes/enemy/boss.tscn | 22 +-- scenes/enemy/enemy.tscn | 22 +-- scenes/player/player.tscn | 15 +- scenes/portal/portal.tscn | 12 +- scenes/world.tscn | 42 +++++ scripts/abilities/ability.gd | 104 ----------- scripts/components/health.gd | 45 ----- scripts/components/health.gd.uid | 1 - scripts/components/healthbar.gd | 39 ++-- scripts/components/shield.gd | 57 ------ scripts/components/shield.gd.uid | 1 - scripts/components/spawner.gd | 44 ----- scripts/components/spawner.gd.uid | 1 - scripts/dungeon/dungeon_manager.gd | 2 +- scripts/enemy/enemy.gd | 39 +--- scripts/enemy/enemy_aggro.gd | 82 --------- scripts/enemy/enemy_aggro.gd.uid | 1 - scripts/enemy/enemy_combat.gd | 26 --- scripts/enemy/enemy_combat.gd.uid | 1 - scripts/enemy/enemy_movement.gd | 16 +- scripts/event_bus.gd | 36 +++- scripts/game_state.gd | 24 +-- scripts/player/combat.gd | 90 +--------- scripts/player/movement.gd | 17 +- scripts/player/player.gd | 20 +++ scripts/player/respawn.gd | 46 ----- scripts/player/respawn.gd.uid | 1 - scripts/portal/portal.gd | 6 + .../{entity_stats.gd => base_stats.gd} | 2 +- scripts/resources/base_stats.gd.uid | 1 + scripts/resources/boss_stats.gd | 2 + scripts/resources/boss_stats.gd.uid | 1 + scripts/resources/enemy_stats.gd | 12 ++ scripts/resources/enemy_stats.gd.uid | 1 + scripts/resources/entity_stats.gd.uid | 1 - scripts/resources/player_stats.gd | 10 ++ scripts/resources/player_stats.gd.uid | 1 + scripts/resources/portal_stats.gd | 5 + scripts/resources/portal_stats.gd.uid | 1 + scripts/stats.gd | 53 ++++++ scripts/stats.gd.uid | 1 + scripts/systems/ability_system.gd | 169 ++++++++++++++++++ scripts/systems/ability_system.gd.uid | 1 + scripts/systems/aggro_system.gd | 130 ++++++++++++++ scripts/systems/aggro_system.gd.uid | 1 + scripts/systems/buff_system.gd | 37 ++++ scripts/systems/buff_system.gd.uid | 1 + scripts/systems/cooldown_system.gd | 73 ++++++++ scripts/systems/cooldown_system.gd.uid | 1 + scripts/systems/damage_system.gd | 1 + scripts/systems/damage_system.gd.uid | 1 + scripts/systems/enemy_ai_system.gd | 36 ++++ scripts/systems/enemy_ai_system.gd.uid | 1 + scripts/systems/health_system.gd | 49 +++++ scripts/systems/health_system.gd.uid | 1 + scripts/systems/respawn_system.gd | 48 +++++ scripts/systems/respawn_system.gd.uid | 1 + scripts/systems/shield_system.gd | 38 ++++ scripts/systems/shield_system.gd.uid | 1 + scripts/systems/spawn_system.gd | 52 ++++++ scripts/systems/spawn_system.gd.uid | 1 + scripts/world/portal_spawner.gd | 1 - 70 files changed, 1016 insertions(+), 732 deletions(-) delete mode 100644 scripts/components/health.gd delete mode 100644 scripts/components/health.gd.uid delete mode 100644 scripts/components/shield.gd delete mode 100644 scripts/components/shield.gd.uid delete mode 100644 scripts/components/spawner.gd delete mode 100644 scripts/components/spawner.gd.uid delete mode 100644 scripts/enemy/enemy_aggro.gd delete mode 100644 scripts/enemy/enemy_aggro.gd.uid delete mode 100644 scripts/enemy/enemy_combat.gd delete mode 100644 scripts/enemy/enemy_combat.gd.uid delete mode 100644 scripts/player/respawn.gd delete mode 100644 scripts/player/respawn.gd.uid rename scripts/resources/{entity_stats.gd => base_stats.gd} (89%) create mode 100644 scripts/resources/base_stats.gd.uid create mode 100644 scripts/resources/boss_stats.gd create mode 100644 scripts/resources/boss_stats.gd.uid create mode 100644 scripts/resources/enemy_stats.gd create mode 100644 scripts/resources/enemy_stats.gd.uid delete mode 100644 scripts/resources/entity_stats.gd.uid create mode 100644 scripts/resources/player_stats.gd create mode 100644 scripts/resources/player_stats.gd.uid create mode 100644 scripts/resources/portal_stats.gd create mode 100644 scripts/resources/portal_stats.gd.uid create mode 100644 scripts/stats.gd create mode 100644 scripts/stats.gd.uid create mode 100644 scripts/systems/ability_system.gd create mode 100644 scripts/systems/ability_system.gd.uid create mode 100644 scripts/systems/aggro_system.gd create mode 100644 scripts/systems/aggro_system.gd.uid create mode 100644 scripts/systems/buff_system.gd create mode 100644 scripts/systems/buff_system.gd.uid create mode 100644 scripts/systems/cooldown_system.gd create mode 100644 scripts/systems/cooldown_system.gd.uid create mode 100644 scripts/systems/damage_system.gd create mode 100644 scripts/systems/damage_system.gd.uid create mode 100644 scripts/systems/enemy_ai_system.gd create mode 100644 scripts/systems/enemy_ai_system.gd.uid create mode 100644 scripts/systems/health_system.gd create mode 100644 scripts/systems/health_system.gd.uid create mode 100644 scripts/systems/respawn_system.gd create mode 100644 scripts/systems/respawn_system.gd.uid create mode 100644 scripts/systems/shield_system.gd create mode 100644 scripts/systems/shield_system.gd.uid create mode 100644 scripts/systems/spawn_system.gd create mode 100644 scripts/systems/spawn_system.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index 680b403..e7bb0ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,37 +12,41 @@ Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare n - Keine Debug-Prints im finalen Code (nur temporär zum Testen) ## Architektur -- **Systeme berechnen und entscheiden**, Szenen rendern und senden/empfangen -- **Event-Flow**: Input → Intention-Event → System → Ergebnis-Event → Node -- **Systeme** sind Scene-Nodes (nicht Autoloads), gefunden über Gruppen -- **Zwischen Szenen**: Kommunikation über EventBus (Autoload). Szenen kennen sich nicht. -- **Innerhalb einer Szene**: Modulare Skripte als Child-Nodes, Zugriff auf Geschwister-Nodes erlaubt. -- **Autoloads**: EventBus (Signals), GameState (Spielerzustand zwischen Szenenwechseln) -- **Gruppen**: "player", "enemies", "portals", "boss" -- **Resources** für statische Konfiguration (Stats, Abilities), **Nodes** für laufenden Zustand +- **Stats (Model)**: Autoload, zentrale Datenhaltung aller Entity-Attribute. Basiswerte aus Resources. +- **Systeme (Controller)**: Scene-Nodes in world.tscn/dungeon.tscn, lesen/schreiben über Stats. +- **Szenen (Views)**: Rendern, Input senden, Events empfangen. Kein Gameplay-State. +- **EventBus (Signals)**: Autoload, Kommunikation zwischen Szenen und Systemen. +- **Event-Flow**: Szene → Input → EventBus → System → Stats → EventBus → Szene +- **Zwischen Szenen**: Kommunikation über EventBus. Szenen kennen sich nicht. +- **Innerhalb einer Szene**: Zugriff auf Geschwister-Nodes erlaubt. +- **Autoloads**: EventBus (Signals), Stats (Entity-Daten), GameState (Szene + Position) +- **Gruppen**: "player", "enemies", "portals", "boss", "cooldown_system" +- **Resources** für Basiswerte (Stats, Abilities), **Stats Autoload** für Laufzeitwerte ## Projektstruktur - `scenes/` — .tscn Dateien - - `world.tscn` — Hauptszene (100x100m, Taverne in Mitte) + - `world.tscn` — Hauptszene (100x100m, Taverne, 10 Systeme) - `player/player.tscn` — Spieler - `enemy/enemy.tscn` — Gegner - - `enemy/boss.tscn` — Boss (eigene Szene, erbt von Enemy) - - `portal/portal.tscn` — Portal (Gegner-Spawner) - - `portal/gate.tscn` — Gate (Teleporter, konfigurierbar: Dungeon-Eingang oder Exit) - - `dungeon/dungeon.tscn` — Dungeon (15x90m Schlauch, 4 Gegnergruppen + Boss) + - `enemy/boss.tscn` — Boss + - `portal/portal.tscn` — Portal + - `portal/gate.tscn` — Gate (Teleporter) + - `dungeon/dungeon.tscn` — Dungeon (15x90m, 10 Systeme) - `hud/hud.tscn` — HUD -- `scripts/systems/` — Zentrale Systeme (health, shield, respawn, ability, cooldown, damage, buff, aggro, enemy_ai, spawn) +- `scripts/systems/` — 10 Systeme (health, shield, damage, ability, cooldown, aggro, enemy_ai, respawn, spawn, buff) - `scripts/player/` — Spieler-Skripte (player, camera, movement, combat, targeting, role, hud) - `scripts/enemy/` — Gegner-Skripte (enemy, enemy_movement, boss) - `scripts/portal/` — Portal + Gate (portal, gate) - `scripts/dungeon/` — Dungeon-Logik (dungeon_manager) -- `scripts/components/` — Wiederverwendbare Komponenten (health, shield, healthbar) -- `scripts/abilities/` — Ability-System (ability, ability_set) -- `scripts/resources/` — Resource-Klassen (entity_stats) +- `scripts/components/` — Healthbar (healthbar) +- `scripts/abilities/` — Ability-Daten (ability, ability_set) +- `scripts/resources/` — Resource-Klassen (base_stats, player_stats, enemy_stats, boss_stats, portal_stats) - `scripts/event_bus.gd` — Globale Signals -- `scripts/game_state.gd` — Spielerzustand zwischen Szenenwechseln +- `scripts/stats.gd` — Zentrale Entity-Datenhaltung +- `scripts/game_state.gd` — Szene + Position zwischen Szenenwechseln +- `scripts/world/portal_spawner.gd` — Portal-Spawning - `resources/stats/` — Stats .tres (player_stats, enemy_stats, portal_stats, boss_stats) -- `resources/abilities/` — Ability .tres pro Rolle (single_attack, tank_single, healer_single, etc.) +- `resources/abilities/` — Ability .tres pro Rolle - `resources/ability_sets/` — AbilitySet .tres pro Rolle (tank_set, damage_set, healer_set) ## Planungsdokument @@ -81,11 +85,11 @@ Unter `~/Documents/2026/projekte/mmo/infosammlung/` liegen die originalen Design - Heiler: Heilt statt schadet (Single, AOE, Ult), Heal-Passive, AOE macht Schaden ## Szenenwechsel -- GameState Autoload speichert Spielerzustand (HP, Shield, Rolle) zwischen Szenen +- Stats Autoload cached Spieler-Werte automatisch bei Szenenwechsel +- GameState speichert Rolle + Position - Gate (Eingang): save_player → Dungeon laden -- Gate (Exit): save_player, returning_from_dungeon → Welt laden, Spieler bei Gate-Position -- Boss-Tod: dungeon_cleared → Welt laden, Spieler bei Taverne, Gates weg -- PortalSpawner stellt Gate wieder her wenn portal_position gesetzt und Boss noch lebt +- Gate (Exit): returning_from_dungeon → Welt laden, Spieler bei Gate-Position +- Boss-Tod: dungeon_cleared → Welt laden, Cache geleert, Spieler bei Taverne mit vollen HP ## Workflow mit dem User - **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren diff --git a/plan.md b/plan.md index c10dca4..5db5ad7 100644 --- a/plan.md +++ b/plan.md @@ -29,17 +29,12 @@ - event_bus.gd — Autoload-Singleton, globale Signals - Intentionen (Input → System): - ability_use_requested(player, ability_index) — Spieler will Ability nutzen - - auto_attack_tick(attacker) — Auto-Attack bereit - - target_requested(player, target) — Spieler will Ziel anvisieren - enemy_detected(enemy, player) — Spieler in Detection-Area -- Ergebnisse (System → Node): - - combat_state_changed(player, in_combat) — Kampfstatus geändert (AggroSystem) - - enemy_state_changed(enemy, new_state) — Gegner-State geändert (EnemyAISystem) - - enemy_target_changed(enemy, target) — Gegner-Ziel geändert (AggroSystem) - Kampf: - attack_executed(attacker, position, direction, damage) — Angriff wurde ausgeführt - damage_dealt(attacker, target, damage) — Schaden wurde verteilt - - damage_requested(attacker, target, amount) — Schaden zwischen Szenen anfordern + - damage_requested(attacker, target, amount) — Schaden anfordern + - heal_requested(healer, target, amount) — Heilung anfordern - Entity: - entity_died(entity) — Entity ist gestorben - health_changed(entity, current, max) — Leben hat sich verändert @@ -54,8 +49,6 @@ - cooldown_tick(cooldowns, max_cooldowns, gcd_timer) — Cooldown-Update für HUD - Buff: - buff_changed(entity, stat, value) — Buff hat sich verändert -- Regeneration: - - regeneration_changed(entity, current, max) — Regeneration hat sich verändert - Gegner: - enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert - Portal: @@ -63,6 +56,8 @@ - portal_defeated(portal) — Portal besiegt, wird Gate - Dungeon: - dungeon_cleared() — Boss tot, Dungeon gesäubert +- Reserviert (definiert, noch nicht genutzt): + - auto_attack_tick, target_requested, combat_state_changed, enemy_state_changed, enemy_target_changed, regeneration_changed # Resources ## BaseStats @@ -89,59 +84,53 @@ - Verändert Ability (Element, Beruf, Prestige) # Systeme -- Werden in der Hauptszene instanziert +- In jeder Root-Szene instanziert (world.tscn, dungeon.tscn) - Entities registrieren/deregistrieren sich bei Stats - Systeme lesen/schreiben über Stats ### HealthSystem (health_system.gd) -- Leben (health) und Lebensregeneration (regeneration) berechnen -- Tod bei 0 HP +- Leben und Lebensregeneration berechnen, Tod bei 0 HP - Listener: damage_requested, heal_requested -- Event: health_changed, regeneration_changed, entity_died +- Event: health_changed, entity_died ### ShieldSystem (shield_system.gd) -- Schild (shield) und Schildregeneration (shield_regeneration) berechnen -- Wie zweite Lebensleiste +- Schild und Schildregeneration berechnen +- absorb(entity, amount) → remaining damage - Event: shield_changed, shield_broken, shield_regenerated ### RespawnSystem (respawn_system.gd) - Respawn bei Taverne mit vollen Leben und Schild -- Tod-Timer (3s) - Listener: entity_died - Event: respawn_tick, player_respawned ### AbilitySystem (ability_system.gd) -- Single, AOE, Utility, Ult -- Auto-Attack -- Listener: ability_use_requested, auto_attack_tick -- Event: attack_executed -## BuffSystem (buff_system.gd) -- Passive +- 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 +- Event: buff_changed, shield_changed ### CooldownSystem (cooldown_system.gd) -- Cooldown-Tracking, GCD -- Listener: attack_executed +- Cooldown-Tracking, GCD, AA-Timer per Entity +- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem - Event: cooldown_tick ### DamageSystem (damage_system.gd) -- Schadensberechnung -- Listener: attack_executed -- Event: damage_requested, heal_requested +- Reserviert für spätere Schadensberechnung (aktuell leer) ### AggroSystem (aggro_system.gd) -- Aggro-Tabellen, Decay, Zielwahl, Kampfstatus, Nearby-Alerting -- Listener: damage_dealt, heal_requested, entity_died, enemy_detected, target_requested -- Event: target_changed, combat_state_changed, enemy_target_changed, enemy_engaged +- Aggro-Tabellen, Decay, Zielwahl, Nearby-Alerting +- Listener: damage_dealt, heal_requested, entity_died, enemy_detected +- Event: enemy_engaged ### EnemyAISystem (enemy_ai_system.gd) -- Gegner-States (Idle/Chase/Attack/Return), Bewegungsbefehle -- Listener: enemy_target_changed, entity_died -- Event: enemy_state_changed +- ATTACK-State: Range-Check, Timer, Schaden +- Iteriert Enemies in _physics_process +- Event: damage_requested ### SpawnSystem (spawn_system.gd) -- Entity-Erstellung, Portal/Gate-Spawning, Dungeon-Clear -- Export: mode ("world" / "dungeon") -- Listener: entity_died, health_changed -- Event: portal_spawn, portal_defeated, dungeon_cleared +- Portal-HP-Schwellen-Spawning +- Listener: health_changed, entity_died +- Event: portal_spawn # Szenen - Szenen sind Views — rendern, Input senden, Events empfangen - Kein Gameplay-State in Szenen (liegt in Stats Autoload) - Entities registrieren sich bei Stats in _ready(), deregistrieren in _exit_tree() -- Spieler und HUD wandern beim Szenenwechsel mit (aus Welt entfernt, im Dungeon eingefügt) +- Stats cached Spieler-Werte automatisch bei Szenenwechsel (player_cache) ## Welt - world.tscn — Hauptszene (100x100m) @@ -163,7 +152,7 @@ - CameraPivot (Node3D, camera.gd) - Camera3D - Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats - - Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested, auto_attack_tick + - Combat (Node, combat.gd) — Input-Handler, emittiert ability_use_requested - Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed - Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested - player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn @@ -215,13 +204,16 @@ ## Dungeon - dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht) + - Systems (alle 10 Systeme, temporär bis Welt parallel läuft) - NavigationRegion3D - Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch) + - Spieler (Instanz von player.tscn) + - HUD (Instanz von hud.tscn) - Gegnergruppen (4x4 Gegner) - Boss (Instanz von boss.tscn) - Exit-Gate (Instanz von gate.tscn, is_exit=true) - DungeonManager (Node, dungeon_manager.gd) -- Keine eigenen Systems — Welt läuft weiter, Systems der Welt verarbeiten alle Entities +- Eigene Systems bis Welt parallel läuft (geplant: Reparenting) ## HUD - hud.tscn — CanvasLayer diff --git a/project.godot b/project.godot index 7b907ed..fcf76ef 100644 --- a/project.godot +++ b/project.godot @@ -18,6 +18,7 @@ config/icon="res://icon.svg" [autoload] EventBus="*res://scripts/event_bus.gd" +Stats="*res://scripts/stats.gd" GameState="*res://scripts/game_state.gd" [dotnet] diff --git a/resources/stats/boss_stats.tres b/resources/stats/boss_stats.tres index 9b27049..18a6a42 100644 --- a/resources/stats/boss_stats.tres +++ b/resources/stats/boss_stats.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] +[gd_resource type="Resource" script_class="BossStats" load_steps=2 format=3] -[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] +[ext_resource type="Script" path="res://scripts/resources/boss_stats.gd" id="1"] [resource] script = ExtResource("1") @@ -9,3 +9,12 @@ health_regen = 0.0 max_shield = 100.0 shield_regen_delay = 5.0 shield_regen_time = 8.0 +speed = 3.0 +attack_range = 2.0 +attack_cooldown = 1.5 +attack_damage = 5.0 +regen_fast = 0.1 +regen_slow = 0.01 +aggro_decay = 1.0 +portal_radius = 10.0 +alert_radius = 3.0 diff --git a/resources/stats/enemy_stats.tres b/resources/stats/enemy_stats.tres index acdccd4..7428f22 100644 --- a/resources/stats/enemy_stats.tres +++ b/resources/stats/enemy_stats.tres @@ -1,11 +1,7 @@ -[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] +[gd_resource type="Resource" script_class="EnemyStats" format=3 uid="uid://cj1shmjwf0xeo"] -[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] +[ext_resource type="Script" uid="uid://bh2uuuvl30y0x" path="res://scripts/resources/enemy_stats.gd" id="1"] [resource] script = ExtResource("1") -max_health = 100.0 -health_regen = 0.0 max_shield = 50.0 -shield_regen_delay = 3.0 -shield_regen_time = 5.0 diff --git a/resources/stats/player_stats.tres b/resources/stats/player_stats.tres index f3b2589..7a1136a 100644 --- a/resources/stats/player_stats.tres +++ b/resources/stats/player_stats.tres @@ -1,11 +1,8 @@ -[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] +[gd_resource type="Resource" script_class="PlayerStats" format=3 uid="uid://btd0g0oiulssq"] -[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] +[ext_resource type="Script" uid="uid://ypyntbavbsto" path="res://scripts/resources/player_stats.gd" id="1"] [resource] script = ExtResource("1") -max_health = 100.0 health_regen = 1.0 max_shield = 50.0 -shield_regen_delay = 3.0 -shield_regen_time = 5.0 diff --git a/resources/stats/portal_stats.tres b/resources/stats/portal_stats.tres index a07ef45..1273561 100644 --- a/resources/stats/portal_stats.tres +++ b/resources/stats/portal_stats.tres @@ -1,9 +1,7 @@ -[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] +[gd_resource type="Resource" script_class="PortalStats" format=3 uid="uid://be2vv5u0jw0yw"] -[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] +[ext_resource type="Script" uid="uid://bioid3s5oftxs" path="res://scripts/resources/portal_stats.gd" id="1"] [resource] script = ExtResource("1") max_health = 500.0 -health_regen = 0.0 -max_shield = 0.0 diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn index bb4c764..a33f1cc 100644 --- a/scenes/dungeon/dungeon.tscn +++ b/scenes/dungeon/dungeon.tscn @@ -6,6 +6,16 @@ [ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"] [ext_resource type="Script" path="res://scripts/dungeon/dungeon_manager.gd" id="dungeon_manager"] [ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"] +[ext_resource type="Script" path="res://scripts/systems/health_system.gd" id="health_system"] +[ext_resource type="Script" path="res://scripts/systems/shield_system.gd" id="shield_system"] +[ext_resource type="Script" path="res://scripts/systems/damage_system.gd" id="damage_system"] +[ext_resource type="Script" path="res://scripts/systems/ability_system.gd" id="ability_system"] +[ext_resource type="Script" path="res://scripts/systems/cooldown_system.gd" id="cooldown_system"] +[ext_resource type="Script" path="res://scripts/systems/aggro_system.gd" id="aggro_system"] +[ext_resource type="Script" path="res://scripts/systems/enemy_ai_system.gd" id="enemy_ai_system"] +[ext_resource type="Script" path="res://scripts/systems/respawn_system.gd" id="respawn_system"] +[ext_resource type="Script" path="res://scripts/systems/spawn_system.gd" id="spawn_system"] +[ext_resource type="Script" path="res://scripts/systems/buff_system.gd" id="buff_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) @@ -39,6 +49,38 @@ size = Vector3(0.5, 3, 90) [node name="Dungeon" type="Node3D"] +[node name="Systems" type="Node" parent="."] + +[node name="HealthSystem" type="Node" parent="Systems"] +script = ExtResource("health_system") + +[node name="ShieldSystem" type="Node" parent="Systems"] +script = ExtResource("shield_system") + +[node name="DamageSystem" type="Node" parent="Systems"] +script = ExtResource("damage_system") + +[node name="AbilitySystem" type="Node" parent="Systems"] +script = ExtResource("ability_system") + +[node name="CooldownSystem" type="Node" parent="Systems"] +script = ExtResource("cooldown_system") + +[node name="AggroSystem" type="Node" parent="Systems"] +script = ExtResource("aggro_system") + +[node name="EnemyAISystem" type="Node" parent="Systems"] +script = ExtResource("enemy_ai_system") + +[node name="RespawnSystem" type="Node" parent="Systems"] +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="NavigationRegion3D" type="NavigationRegion3D" parent="."] navigation_mesh = SubResource("NavigationMesh_1") diff --git a/scenes/enemy/boss.tscn b/scenes/enemy/boss.tscn index 3d7e9d6..68df57e 100644 --- a/scenes/enemy/boss.tscn +++ b/scenes/enemy/boss.tscn @@ -1,12 +1,8 @@ [gd_scene load_steps=6 format=3] [ext_resource type="Script" path="res://scripts/enemy/boss.gd" id="1"] -[ext_resource type="Script" path="res://scripts/components/health.gd" id="2"] -[ext_resource type="Script" path="res://scripts/components/shield.gd" id="3"] [ext_resource type="Script" path="res://scripts/components/healthbar.gd" id="4"] [ext_resource type="Script" path="res://scripts/enemy/enemy_movement.gd" id="5"] -[ext_resource type="Script" path="res://scripts/enemy/enemy_combat.gd" id="6"] -[ext_resource type="Script" path="res://scripts/enemy/enemy_aggro.gd" id="7"] [ext_resource type="Resource" path="res://resources/stats/boss_stats.tres" id="8"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] @@ -23,9 +19,6 @@ bg_color = Color(0.3, 0.1, 0.1, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] bg_color = Color(0.2, 0.8, 0.2, 1) -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill_aggro"] -bg_color = Color(0.2, 0.4, 0.9, 1) - [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"] bg_color = Color(0.1, 0.1, 0.3, 1) @@ -45,6 +38,7 @@ radius = 8.0 [node name="Boss" type="CharacterBody3D"] script = ExtResource("1") +stats = ExtResource("8") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("CapsuleShape3D_1") @@ -53,14 +47,6 @@ shape = SubResource("CapsuleShape3D_1") transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0) mesh = SubResource("SphereMesh_1") -[node name="Health" type="Node" parent="."] -script = ExtResource("2") -stats = ExtResource("8") - -[node name="Shield" type="Node" parent="."] -script = ExtResource("3") -stats = ExtResource("8") - [node name="HitArea" type="Area3D" parent="."] collision_layer = 4 collision_mask = 0 @@ -73,12 +59,6 @@ shape = SubResource("CapsuleShape3D_2") [node name="EnemyMovement" type="Node" parent="."] script = ExtResource("5") -[node name="EnemyCombat" type="Node" parent="."] -script = ExtResource("6") - -[node name="EnemyAggro" type="Node" parent="."] -script = ExtResource("7") - [node name="DetectionArea" type="Area3D" parent="."] collision_layer = 0 collision_mask = 1 diff --git a/scenes/enemy/enemy.tscn b/scenes/enemy/enemy.tscn index 832fcbb..37dc84f 100644 --- a/scenes/enemy/enemy.tscn +++ b/scenes/enemy/enemy.tscn @@ -1,12 +1,8 @@ [gd_scene load_steps=6 format=3] [ext_resource type="Script" path="res://scripts/enemy/enemy.gd" id="1"] -[ext_resource type="Script" path="res://scripts/components/health.gd" id="2"] -[ext_resource type="Script" path="res://scripts/components/shield.gd" id="3"] [ext_resource type="Script" path="res://scripts/components/healthbar.gd" id="4"] [ext_resource type="Script" path="res://scripts/enemy/enemy_movement.gd" id="5"] -[ext_resource type="Script" path="res://scripts/enemy/enemy_combat.gd" id="6"] -[ext_resource type="Script" path="res://scripts/enemy/enemy_aggro.gd" id="7"] [ext_resource type="Resource" path="res://resources/stats/enemy_stats.tres" id="8"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] @@ -23,9 +19,6 @@ bg_color = Color(0.3, 0.1, 0.1, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] bg_color = Color(0.2, 0.8, 0.2, 1) -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill_aggro"] -bg_color = Color(0.2, 0.4, 0.9, 1) - [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"] bg_color = Color(0.1, 0.1, 0.3, 1) @@ -45,6 +38,7 @@ radius = 8.0 [node name="Enemy" type="CharacterBody3D"] script = ExtResource("1") +stats = ExtResource("8") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("CapsuleShape3D_1") @@ -52,14 +46,6 @@ shape = SubResource("CapsuleShape3D_1") [node name="Mesh" type="MeshInstance3D" parent="."] mesh = SubResource("SphereMesh_1") -[node name="Health" type="Node" parent="."] -script = ExtResource("2") -stats = ExtResource("8") - -[node name="Shield" type="Node" parent="."] -script = ExtResource("3") -stats = ExtResource("8") - [node name="HitArea" type="Area3D" parent="."] collision_layer = 4 collision_mask = 0 @@ -72,12 +58,6 @@ shape = SubResource("CapsuleShape3D_2") [node name="EnemyMovement" type="Node" parent="."] script = ExtResource("5") -[node name="EnemyCombat" type="Node" parent="."] -script = ExtResource("6") - -[node name="EnemyAggro" type="Node" parent="."] -script = ExtResource("7") - [node name="DetectionArea" type="Area3D" parent="."] collision_layer = 0 collision_mask = 1 diff --git a/scenes/player/player.tscn b/scenes/player/player.tscn index a817de2..95f959a 100644 --- a/scenes/player/player.tscn +++ b/scenes/player/player.tscn @@ -4,10 +4,7 @@ [ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scripts/player/camera.gd" id="2"] [ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scripts/player/movement.gd" id="3"] [ext_resource type="Script" uid="uid://d15til6fsxw5b" path="res://scripts/player/combat.gd" id="4"] -[ext_resource type="Script" uid="uid://b053b4fkkeaod" path="res://scripts/components/health.gd" id="5"] -[ext_resource type="Script" uid="uid://bpfw71oprcvou" path="res://scripts/components/shield.gd" id="6"] [ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scripts/player/targeting.gd" id="8"] -[ext_resource type="Script" uid="uid://dw3dtax5bx0of" path="res://scripts/player/respawn.gd" id="9"] [ext_resource type="Script" path="res://scripts/player/role.gd" id="10"] [ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://resources/ability_sets/tank_set.tres" id="11"] [ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://resources/ability_sets/damage_set.tres" id="12"] @@ -24,6 +21,7 @@ height = 1.8 [node name="Player" type="CharacterBody3D" unique_id=1350215040] script = ExtResource("1") +stats = ExtResource("14") [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999] shape = SubResource("CapsuleShape3D_1") @@ -47,17 +45,6 @@ script = ExtResource("4") [node name="Targeting" type="Node" parent="." unique_id=592540710] script = ExtResource("8") -[node name="Health" type="Node" parent="." unique_id=1872357630] -script = ExtResource("5") -stats = ExtResource("14") - -[node name="Shield" type="Node" parent="." unique_id=716948065] -script = ExtResource("6") -stats = ExtResource("14") - -[node name="Respawn" type="Node" parent="." unique_id=1562314386] -script = ExtResource("9") - [node name="Role" type="Node" parent="." unique_id=134158295] script = ExtResource("10") tank_set = ExtResource("11") diff --git a/scenes/portal/portal.tscn b/scenes/portal/portal.tscn index 1ff207a..3332a8f 100644 --- a/scenes/portal/portal.tscn +++ b/scenes/portal/portal.tscn @@ -1,10 +1,7 @@ [gd_scene format=3] [ext_resource type="Script" path="res://scripts/portal/portal.gd" id="1"] -[ext_resource type="Script" path="res://scripts/components/health.gd" id="2"] [ext_resource type="Script" path="res://scripts/components/healthbar.gd" id="3"] -[ext_resource type="Script" path="res://scripts/components/spawner.gd" id="4"] -[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="5"] [ext_resource type="Resource" path="res://resources/stats/portal_stats.tres" id="6"] [sub_resource type="SphereShape3D" id="SphereShape3D_1"] @@ -35,6 +32,7 @@ bg_color = Color(0.2, 0.8, 0.2, 1) [node name="Portal" type="StaticBody3D"] script = ExtResource("1") +stats = ExtResource("6") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("CylinderShape3D_1") @@ -42,10 +40,6 @@ shape = SubResource("CylinderShape3D_1") [node name="Mesh" type="MeshInstance3D" parent="."] mesh = SubResource("CylinderMesh_1") -[node name="Health" type="Node" parent="."] -script = ExtResource("2") -stats = ExtResource("6") - [node name="HitArea" type="Area3D" parent="."] collision_layer = 4 collision_mask = 0 @@ -61,10 +55,6 @@ monitoring = true [node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"] shape = SubResource("SphereShape3D_1") -[node name="Spawner" type="Node" parent="."] -script = ExtResource("4") -spawn_scene = ExtResource("5") - [node name="Healthbar" type="Sprite3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) billboard = 1 diff --git a/scenes/world.tscn b/scenes/world.tscn index aa8c6b3..e865c5b 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -1,8 +1,18 @@ [gd_scene format=3 uid="uid://dy1icabu2ssbw"] +[ext_resource type="Script" uid="uid://h0hts425epc6" path="res://scripts/systems/ability_system.gd" id="ability_system"] +[ext_resource type="Script" uid="uid://cm7ehl2pexcst" path="res://scripts/systems/aggro_system.gd" id="aggro_system"] +[ext_resource type="Script" uid="uid://da2jm0awq2lnh" path="res://scripts/systems/buff_system.gd" id="buff_system"] +[ext_resource type="Script" uid="uid://ddos7mo8rahou" path="res://scripts/systems/cooldown_system.gd" id="cooldown_system"] +[ext_resource type="Script" uid="uid://cbd1bryh0e2dw" path="res://scripts/systems/damage_system.gd" id="damage_system"] +[ext_resource type="Script" uid="uid://bwhxu5586lc1l" path="res://scripts/systems/enemy_ai_system.gd" id="enemy_ai_system"] +[ext_resource type="Script" uid="uid://b3wkn5118dimy" path="res://scripts/systems/health_system.gd" id="health_system"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] [ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] [ext_resource type="Script" uid="uid://cskx6o07iukwh" path="res://scripts/world/portal_spawner.gd" id="portal_spawner"] +[ext_resource type="Script" uid="uid://b1qkvoqvmd21h" path="res://scripts/systems/respawn_system.gd" id="respawn_system"] +[ext_resource type="Script" uid="uid://rsnpuf77o0sn" path="res://scripts/systems/shield_system.gd" id="shield_system"] +[ext_resource type="Script" uid="uid://c84voxmnaifyt" path="res://scripts/systems/spawn_system.gd" id="spawn_system"] [sub_resource type="NavigationMesh" id="NavigationMesh_1"] vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5) @@ -41,6 +51,38 @@ size = Vector3(5, 3, 5) [node name="World" type="Node3D" unique_id=1865233338] +[node name="Systems" type="Node" parent="." unique_id=1813416478] + +[node name="HealthSystem" type="Node" parent="Systems" unique_id=221270411] +script = ExtResource("health_system") + +[node name="ShieldSystem" type="Node" parent="Systems" unique_id=1790230220] +script = ExtResource("shield_system") + +[node name="DamageSystem" type="Node" parent="Systems" unique_id=2146323526] +script = ExtResource("damage_system") + +[node name="AbilitySystem" type="Node" parent="Systems" unique_id=391120092] +script = ExtResource("ability_system") + +[node name="CooldownSystem" type="Node" parent="Systems" unique_id=99457358] +script = ExtResource("cooldown_system") + +[node name="AggroSystem" type="Node" parent="Systems" unique_id=1539448343] +script = ExtResource("aggro_system") + +[node name="EnemyAISystem" type="Node" parent="Systems" unique_id=2089718042] +script = ExtResource("enemy_ai_system") + +[node name="RespawnSystem" type="Node" parent="Systems" unique_id=1586865573] +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="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679] navigation_mesh = SubResource("NavigationMesh_1") diff --git a/scripts/abilities/ability.gd b/scripts/abilities/ability.gd index 05e1fd3..fff20d9 100644 --- a/scripts/abilities/ability.gd +++ b/scripts/abilities/ability.gd @@ -13,107 +13,3 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } @export var icon: String = "" @export var is_heal: bool = false @export var passive_stat: String = "damage" - -func execute(player: Node, targeting: Node) -> bool: - var stat: String = "heal" if is_heal else "damage" - var dmg: float = _get_modified_damage(player, damage, stat) - match type: - Type.SINGLE: - return _execute_single(player, targeting, dmg) - Type.AOE: - return _execute_aoe(player, dmg) - Type.UTILITY: - return _execute_utility(player) - Type.ULT: - return _execute_ult(player, targeting, dmg) - return false - -func _get_modified_damage(player: Node, base: float, stat: String = "damage") -> float: - var combat: Node = player.get_node("Combat") - return combat.apply_passive(base, stat) - -func _in_range(player: Node, targeting: Node) -> bool: - if ability_range <= 0: - return true - if is_heal: - return true - if not is_instance_valid(targeting.current_target): - return false - var dist: float = player.global_position.distance_to(targeting.current_target.global_position) - return dist <= ability_range - -func _execute_single(player: Node, targeting: Node, dmg: float) -> bool: - if is_heal: - EventBus.heal_requested.emit(player, player, dmg) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - if not _in_range(player, targeting): - return false - if not is_instance_valid(targeting.current_target): - return false - EventBus.damage_requested.emit(player, targeting.current_target, dmg) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - -func _execute_aoe(player: Node, dmg: float) -> bool: - if is_heal: - EventBus.heal_requested.emit(player, player, dmg) - var players := player.get_tree().get_nodes_in_group("player") - for p in players: - if p != player and is_instance_valid(p): - var dist: float = player.global_position.distance_to(p.global_position) - if dist <= ability_range: - EventBus.heal_requested.emit(player, p, dmg) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - var hit := false - var enemies := player.get_tree().get_nodes_in_group("enemies") - for enemy in enemies: - var dist: float = player.global_position.distance_to(enemy.global_position) - if dist <= ability_range: - EventBus.damage_requested.emit(player, enemy, dmg) - hit = true - if hit: - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return hit - -func _execute_utility(player: Node) -> bool: - var shield: Node = player.get_node_or_null("Shield") - if shield: - if damage > 0: - shield.current_shield = shield.max_shield * (damage / 100.0) - else: - if shield.current_shield >= shield.max_shield: - return false - shield.current_shield = shield.max_shield - EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield) - return true - return false - -func _execute_ult(player: Node, targeting: Node, dmg: float) -> bool: - if is_heal: - EventBus.heal_requested.emit(player, player, dmg) - var players := player.get_tree().get_nodes_in_group("player") - var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range - for p in players: - if p != player and is_instance_valid(p): - var dist: float = player.global_position.distance_to(p.global_position) - if dist <= aoe_range: - EventBus.heal_requested.emit(player, p, dmg * 0.4) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - if not _in_range(player, targeting): - return false - if not is_instance_valid(targeting.current_target): - return false - var target: Node3D = targeting.current_target - EventBus.damage_requested.emit(player, target, dmg * 5.0) - var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range - var enemies := player.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: - EventBus.damage_requested.emit(player, enemy, dmg * 2.0) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0) - return true diff --git a/scripts/components/health.gd b/scripts/components/health.gd deleted file mode 100644 index 4f2c173..0000000 --- a/scripts/components/health.gd +++ /dev/null @@ -1,45 +0,0 @@ -extends Node - -@export var stats: EntityStats -var max_health: float -var health_regen: float -var current_health: float - -func _ready() -> void: - max_health = stats.max_health - health_regen = stats.health_regen - current_health = max_health - EventBus.damage_requested.connect(_on_damage_requested) - EventBus.heal_requested.connect(_on_heal_requested) - -func _process(delta: float) -> void: - if current_health > 0 and current_health < max_health and health_regen > 0: - current_health = min(current_health + health_regen * delta, max_health) - EventBus.health_changed.emit(get_parent(), current_health, max_health) - -func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: - if target != get_parent(): - return - var remaining: float = amount - var shield: Node = get_parent().get_node_or_null("Shield") - if shield: - remaining = shield.absorb(remaining) - EventBus.damage_dealt.emit(attacker, get_parent(), amount) - if remaining > 0: - take_damage(remaining, attacker) - -func take_damage(amount: float, attacker: Node) -> void: - current_health -= amount - if current_health <= 0: - current_health = 0 - EventBus.health_changed.emit(get_parent(), current_health, max_health) - if current_health <= 0: - EventBus.entity_died.emit(get_parent()) - -func heal(amount: float) -> void: - current_health = min(current_health + amount, max_health) - EventBus.health_changed.emit(get_parent(), current_health, max_health) - -func _on_heal_requested(healer: Node, target: Node, amount: float) -> void: - if target == get_parent(): - heal(amount) diff --git a/scripts/components/health.gd.uid b/scripts/components/health.gd.uid deleted file mode 100644 index 82be2a2..0000000 --- a/scripts/components/health.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b053b4fkkeaod diff --git a/scripts/components/healthbar.gd b/scripts/components/healthbar.gd index 323c508..17e6b93 100644 --- a/scripts/components/healthbar.gd +++ b/scripts/components/healthbar.gd @@ -3,38 +3,55 @@ extends Sprite3D @onready var viewport: SubViewport = $SubViewport @onready var health_bar: ProgressBar = $SubViewport/HealthBar @onready var border: ColorRect = $SubViewport/Border -@onready var health: Node = get_parent().get_node("Health") @onready var parent_node: Node = get_parent() -var shield: Node = null var shield_bar: ProgressBar = null var style_normal: StyleBoxFlat var style_aggro: StyleBoxFlat func _ready() -> void: texture = viewport.get_texture() - health_bar.max_value = health.max_health - shield = get_parent().get_node_or_null("Shield") shield_bar = $SubViewport.get_node_or_null("ShieldBar") - if shield and shield_bar: - shield_bar.max_value = shield.max_shield - elif shield_bar: - shield_bar.visible = false 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) EventBus.target_changed.connect(_on_target_changed) + EventBus.health_changed.connect(_on_health_changed) + EventBus.shield_changed.connect(_on_shield_changed) + _init_bars() + +func _init_bars() -> void: + var max_health: Variant = Stats.get_stat(parent_node, "max_health") + if max_health != null: + health_bar.max_value = max_health + health_bar.value = Stats.get_stat(parent_node, "health") + var max_shield: Variant = Stats.get_stat(parent_node, "max_shield") + if shield_bar: + if max_shield != null and max_shield > 0: + shield_bar.max_value = max_shield + shield_bar.value = Stats.get_stat(parent_node, "shield") + else: + shield_bar.visible = false func _process(_delta: float) -> void: - health_bar.value = health.current_health - if shield and shield_bar: - shield_bar.value = shield.current_shield var player: Node = get_tree().get_first_node_in_group("player") if player and "target" in parent_node and parent_node.target == player: health_bar.add_theme_stylebox_override("fill", style_aggro) else: health_bar.add_theme_stylebox_override("fill", style_normal) +func _on_health_changed(entity: Node, current: float, max_val: float) -> void: + if entity != parent_node: + return + health_bar.max_value = max_val + health_bar.value = current + +func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: + if entity != parent_node or shield_bar == null: + return + shield_bar.max_value = max_val + shield_bar.value = current + func _on_target_changed(_player: Node, target: Node) -> void: border.visible = (target == get_parent()) diff --git a/scripts/components/shield.gd b/scripts/components/shield.gd deleted file mode 100644 index c3a6e57..0000000 --- a/scripts/components/shield.gd +++ /dev/null @@ -1,57 +0,0 @@ -extends Node - -@export var stats: EntityStats -var max_shield: float -var regen_delay: float -var regen_time: float -var current_shield: float -var regen_timer := 0.0 - -var base_max_shield: float - -func _ready() -> void: - max_shield = stats.max_shield - base_max_shield = max_shield - regen_delay = stats.shield_regen_delay - regen_time = stats.shield_regen_time - current_shield = max_shield - EventBus.role_changed.connect(_on_role_changed) - -func _process(delta: float) -> void: - if max_shield <= 0: - return - if current_shield < max_shield: - regen_timer += delta - if regen_timer >= regen_delay: - current_shield += (max_shield / regen_time) * delta - if current_shield >= max_shield: - current_shield = max_shield - EventBus.shield_regenerated.emit(get_parent()) - EventBus.shield_changed.emit(get_parent(), current_shield, max_shield) - -func _on_role_changed(_player: Node, _role_type: int) -> void: - if get_parent() != _player: - return - var role: Node = get_parent().get_node_or_null("Role") - if not role: - return - var ability_set: AbilitySet = role.get_ability_set() - if not ability_set: - return - max_shield = base_max_shield - for ability in ability_set.abilities: - if ability and ability.type == Ability.Type.PASSIVE and ability.passive_stat == "shield": - max_shield = base_max_shield * (1.0 + ability.damage / 100.0) - current_shield = min(current_shield, max_shield) - EventBus.shield_changed.emit(get_parent(), current_shield, max_shield) - -func absorb(amount: float) -> float: - if current_shield <= 0: - return amount - regen_timer = 0.0 - var absorbed: float = min(amount, current_shield) - current_shield -= absorbed - if current_shield <= 0: - EventBus.shield_broken.emit(get_parent()) - EventBus.shield_changed.emit(get_parent(), current_shield, max_shield) - return amount - absorbed diff --git a/scripts/components/shield.gd.uid b/scripts/components/shield.gd.uid deleted file mode 100644 index add605d..0000000 --- a/scripts/components/shield.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bpfw71oprcvou diff --git a/scripts/components/spawner.gd b/scripts/components/spawner.gd deleted file mode 100644 index f44d734..0000000 --- a/scripts/components/spawner.gd +++ /dev/null @@ -1,44 +0,0 @@ -extends Node - -@export var spawn_scene: PackedScene -@export var spawn_count := 3 -@export var thresholds: Array[float] = [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] - -var triggered: Array[bool] = [] - -@onready var parent: Node3D = get_parent() -@onready var health: Node = get_parent().get_node("Health") - -func _ready() -> void: - triggered.resize(thresholds.size()) - triggered.fill(false) - -func _process(_delta: float) -> void: - if not spawn_scene or health.current_health <= 0: - return - var ratio: float = health.current_health / health.max_health - for i in range(thresholds.size()): - if not triggered[i] and ratio <= thresholds[i]: - triggered[i] = true - _spawn() - -func _spawn() -> void: - var spawned: Array = [] - for j in range(spawn_count): - var entity: Node = spawn_scene.instantiate() - var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) - parent.get_parent().add_child(entity) - entity.global_position = parent.global_position + offset - if "spawn_position" in entity: - entity.spawn_position = parent.global_position - if "portal" in entity: - entity.portal = parent - spawned.append(entity) - var player: Node = get_tree().get_first_node_in_group("player") - if player: - var dist: float = parent.global_position.distance_to(player.global_position) - if dist <= 10.0: - for entity in spawned: - if entity.has_method("_engage"): - entity._engage(player) - EventBus.portal_spawn.emit(parent, spawned) diff --git a/scripts/components/spawner.gd.uid b/scripts/components/spawner.gd.uid deleted file mode 100644 index 1e2c351..0000000 --- a/scripts/components/spawner.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cm2s3xkmuesey diff --git a/scripts/dungeon/dungeon_manager.gd b/scripts/dungeon/dungeon_manager.gd index 68bd460..7fb9d36 100644 --- a/scripts/dungeon/dungeon_manager.gd +++ b/scripts/dungeon/dungeon_manager.gd @@ -11,6 +11,6 @@ func _on_entity_died(entity: Node) -> void: await get_tree().create_timer(2.0).timeout GameState.dungeon_cleared = true GameState.returning_from_dungeon = false - GameState.clear_player() + GameState.clear() EventBus.dungeon_cleared.emit() get_tree().change_scene_to_file("res://scenes/world.tscn") diff --git a/scripts/enemy/enemy.gd b/scripts/enemy/enemy.gd index a71bd55..f97e5ba 100644 --- a/scripts/enemy/enemy.gd +++ b/scripts/enemy/enemy.gd @@ -2,56 +2,35 @@ extends CharacterBody3D enum State { IDLE, CHASE, ATTACK, RETURN } +@export var stats: BaseStats + var state: int = State.IDLE var target: Node3D = null var spawn_position: Vector3 var portal: Node3D = null var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") -@onready var health: Node = $Health - func _ready() -> void: spawn_position = global_position add_to_group("enemies") + Stats.register(self, stats) EventBus.entity_died.connect(_on_entity_died) +func _exit_tree() -> void: + Stats.deregister(self) + func _on_entity_died(entity: Node) -> void: if entity == self: queue_free() - elif entity == target: - target = null - state = State.RETURN func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= gravity * delta move_and_slide() -func _engage(new_target: Node3D) -> void: - if state == State.CHASE or state == State.ATTACK: - return - target = new_target - state = State.CHASE - var aggro: Node = get_node_or_null("EnemyAggro") - if aggro: - aggro.add_aggro(new_target, 1.0) - _alert_nearby() - -func _alert_nearby() -> void: - var enemies := get_tree().get_nodes_in_group("enemies") - for enemy in enemies: - if enemy != self and is_instance_valid(enemy) and "state" in enemy: - if enemy.state == enemy.State.IDLE: - var dist: float = global_position.distance_to(enemy.global_position) - if dist <= 3.0: - enemy._engage(target) - func _on_detection_area_body_entered(body: Node3D) -> void: if body is CharacterBody3D and body.name == "Player": - _engage(body) - EventBus.enemy_engaged.emit(self, body) + EventBus.enemy_detected.emit(self, body) -func _on_detection_area_body_exited(body: Node3D) -> void: - if body == target and state == State.CHASE: - state = State.RETURN - target = null +func _on_detection_area_body_exited(_body: Node3D) -> void: + pass diff --git a/scripts/enemy/enemy_aggro.gd b/scripts/enemy/enemy_aggro.gd deleted file mode 100644 index 1440971..0000000 --- a/scripts/enemy/enemy_aggro.gd +++ /dev/null @@ -1,82 +0,0 @@ -extends Node - -const AGGRO_DECAY := 1.0 -const PORTAL_RADIUS := 10.0 -var aggro_table: Dictionary = {} -var seconds_outside := 0.0 - -@onready var enemy: CharacterBody3D = get_parent() - -func _ready() -> void: - EventBus.damage_dealt.connect(_on_damage_dealt) - EventBus.entity_died.connect(_on_entity_died) - EventBus.heal_requested.connect(_on_heal_requested) - -func _process(delta: float) -> void: - var outside_portal := false - if enemy.portal and is_instance_valid(enemy.portal): - var dist_to_portal: float = enemy.global_position.distance_to(enemy.portal.global_position) - if dist_to_portal > PORTAL_RADIUS: - outside_portal = true - seconds_outside += delta - else: - seconds_outside = 0.0 - - for player in aggro_table.keys(): - var decay: float = AGGRO_DECAY * delta - if outside_portal: - var bonus_decay: float = aggro_table[player] * 0.01 * pow(2, seconds_outside) * delta - decay += bonus_decay - aggro_table[player] -= decay - # Im Portal-Radius: Aggro bleibt bei mindestens 1 - if not outside_portal 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 aggro_table[player] < 1.0: - aggro_table[player] = 1.0 - if aggro_table[player] <= 0: - aggro_table.erase(player) - - var top_target: Node = _get_top_target() - if top_target and top_target != enemy.target: - enemy.target = top_target - if enemy.state == enemy.State.IDLE or enemy.state == enemy.State.RETURN: - enemy.state = enemy.State.CHASE - elif not top_target and enemy.state != enemy.State.IDLE and enemy.state != enemy.State.RETURN: - enemy.target = null - enemy.state = enemy.State.RETURN - -func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: - if target != enemy: - 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(attacker, amount * multiplier) - -func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void: - if not healer.is_in_group("player"): - return - if healer in aggro_table: - add_aggro(healer, amount * 0.5) - -func _on_entity_died(entity: Node) -> void: - aggro_table.erase(entity) - -func add_aggro(player: Node, amount: float) -> void: - if player in aggro_table: - aggro_table[player] += amount - else: - aggro_table[player] = amount - -func _get_top_target() -> Node: - var top: Node = null - var top_val := 0.0 - for player in aggro_table: - if is_instance_valid(player) and aggro_table[player] > top_val: - top_val = aggro_table[player] - top = player - return top - -func has_aggro_on(player: Node) -> bool: - return _get_top_target() == player diff --git a/scripts/enemy/enemy_aggro.gd.uid b/scripts/enemy/enemy_aggro.gd.uid deleted file mode 100644 index 7a4013e..0000000 --- a/scripts/enemy/enemy_aggro.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bojdohjxr6uef diff --git a/scripts/enemy/enemy_combat.gd b/scripts/enemy/enemy_combat.gd deleted file mode 100644 index d1dc6f1..0000000 --- a/scripts/enemy/enemy_combat.gd +++ /dev/null @@ -1,26 +0,0 @@ -extends Node - -const ATTACK_RANGE := 2.0 -const ATTACK_COOLDOWN := 1.5 -const ATTACK_DAMAGE := 5.0 - -var attack_timer := 0.0 - -@onready var enemy: CharacterBody3D = get_parent() - -func _physics_process(delta: float) -> void: - attack_timer -= delta - if enemy.state != enemy.State.ATTACK: - return - if not is_instance_valid(enemy.target): - enemy.state = enemy.State.RETURN - return - var dist := enemy.global_position.distance_to(enemy.target.global_position) - if dist > ATTACK_RANGE: - enemy.state = enemy.State.CHASE - return - if attack_timer <= 0: - attack_timer = ATTACK_COOLDOWN - EventBus.damage_requested.emit(enemy, enemy.target, ATTACK_DAMAGE) - enemy.velocity.x = 0 - enemy.velocity.z = 0 diff --git a/scripts/enemy/enemy_combat.gd.uid b/scripts/enemy/enemy_combat.gd.uid deleted file mode 100644 index bbca79f..0000000 --- a/scripts/enemy/enemy_combat.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ct4u62xalrjyo diff --git a/scripts/enemy/enemy_movement.gd b/scripts/enemy/enemy_movement.gd index fb46ed3..96031a2 100644 --- a/scripts/enemy/enemy_movement.gd +++ b/scripts/enemy/enemy_movement.gd @@ -49,10 +49,14 @@ func _return_to_spawn(delta: float) -> void: _regenerate(delta) func _regenerate(delta: float) -> void: - var health: Node = enemy.get_node("Health") - if health.current_health < health.max_health: - var rate: float = REGEN_FAST if health.current_health < health.max_health else REGEN_SLOW - if health.current_health >= health.max_health * 0.99: + var health: float = Stats.get_stat(enemy, "health") + var max_health: float = Stats.get_stat(enemy, "max_health") + if health == null or max_health == null: + return + if health < max_health: + var rate: float = REGEN_FAST + if health >= max_health * 0.99: rate = REGEN_SLOW - health.current_health = min(health.current_health + health.max_health * rate * delta, health.max_health) - EventBus.health_changed.emit(enemy, health.current_health, health.max_health) + health = min(health + max_health * rate * delta, max_health) + Stats.set_stat(enemy, "health", health) + EventBus.health_changed.emit(enemy, health, max_health) diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index 7ed5182..c0bcb86 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -1,20 +1,46 @@ 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) +signal damage_requested(attacker, target, amount) +signal heal_requested(healer, target, amount) + +# Entity signal entity_died(entity) +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) signal player_respawned(player) signal role_changed(player, role_type) -signal damage_requested(attacker, target, amount) -signal health_changed(entity, current, max_val) -signal shield_changed(entity, current, max_val) signal respawn_tick(timer) -signal enemy_engaged(enemy, target) signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer) + +# Buff +signal buff_changed(entity, stat, value) + +# Gegner +signal enemy_engaged(enemy, target) + +# Portal signal portal_spawn(portal, enemies) -signal heal_requested(healer, target, amount) signal portal_defeated(portal) + +# Dungeon signal dungeon_cleared() diff --git a/scripts/game_state.gd b/scripts/game_state.gd index 356bbbe..dcaae09 100644 --- a/scripts/game_state.gd +++ b/scripts/game_state.gd @@ -1,42 +1,20 @@ extends Node -var player_health: float = -1.0 -var player_max_health: float = -1.0 -var player_shield: float = -1.0 -var player_max_shield: float = -1.0 var player_role: int = 1 var portal_position: Vector3 = Vector3.ZERO var returning_from_dungeon := false var dungeon_cleared := false func save_player(player: Node) -> void: - var health: Node = player.get_node("Health") - var shield: Node = player.get_node("Shield") var role: Node = player.get_node("Role") - player_health = health.current_health - player_max_health = health.max_health - player_shield = shield.current_shield - player_max_shield = shield.max_shield player_role = role.current_role func restore_player(player: Node) -> void: - if player_health < 0: - return - var health: Node = player.get_node("Health") - var shield: Node = player.get_node("Shield") var role: Node = player.get_node("Role") - health.current_health = player_health - shield.current_shield = player_shield role.set_role(player_role) - EventBus.health_changed.emit(player, health.current_health, health.max_health) - EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield) - -func clear_player() -> void: - player_health = -1.0 - player_shield = -1.0 func clear() -> void: - clear_player() + Stats.clear_player_cache() portal_position = Vector3.ZERO returning_from_dungeon = false dungeon_cleared = false diff --git a/scripts/player/combat.gd b/scripts/player/combat.gd index 5fe5f67..6516da1 100644 --- a/scripts/player/combat.gd +++ b/scripts/player/combat.gd @@ -1,93 +1,9 @@ extends Node -const GCD_TIME := 0.5 -const AA_COOLDOWN := 0.5 - @onready var player: CharacterBody3D = get_parent() -@onready var targeting: Node = get_parent().get_node("Targeting") -@onready var role: Node = get_parent().get_node("Role") - -var abilities: Array = [] -var cooldowns: Array[float] = [0.0, 0.0, 0.0, 0.0, 0.0] -var max_cooldowns: Array[float] = [0.0, 0.0, 0.0, 0.0, 0.0] -var gcd_timer := 0.0 -var aa_timer := 0.0 - -func _ready() -> void: - _load_abilities() - EventBus.role_changed.connect(_on_role_changed) - -func _process(delta: float) -> void: - if gcd_timer > 0: - gcd_timer -= delta - for i in range(cooldowns.size()): - if cooldowns[i] > 0: - cooldowns[i] -= delta - EventBus.cooldown_tick.emit(cooldowns, max_cooldowns, gcd_timer) - _auto_attack(delta) - -func _auto_attack(delta: float) -> void: - aa_timer -= delta - if aa_timer > 0: - return - if not targeting.in_combat or not targeting.current_target: - return - if not is_instance_valid(targeting.current_target): - return - var ability_set: AbilitySet = role.get_ability_set() - if not ability_set: - return - var aa_damage: float = ability_set.aa_damage - var aa_range: float = ability_set.aa_range - var aa_is_heal: bool = ability_set.aa_is_heal - var dmg: float = apply_passive(aa_damage, "heal" if aa_is_heal else "damage") - if aa_is_heal: - EventBus.heal_requested.emit(player, player, dmg) - print("AA Heal: %s an %s" % [dmg, player.name]) - else: - var dist := player.global_position.distance_to(targeting.current_target.global_position) - if dist > aa_range: - return - var target_name: String = targeting.current_target.name - EventBus.damage_requested.emit(player, targeting.current_target, dmg) - print("AA: %s Schaden an %s" % [dmg, target_name]) - aa_timer = AA_COOLDOWN - -func _load_abilities() -> void: - var ability_set: AbilitySet = role.get_ability_set() - if ability_set: - abilities = ability_set.abilities - else: - abilities = [] - cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0] - max_cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0] - gcd_timer = 0.0 func _unhandled_input(event: InputEvent) -> void: - for i in range(min(abilities.size(), 5)): - if event.is_action_pressed("ability_%s" % (i + 1)) and abilities[i]: - if abilities[i].type == Ability.Type.PASSIVE: - return - if cooldowns[i] > 0: - return - if abilities[i].uses_gcd and gcd_timer > 0: - return - var success: bool = abilities[i].execute(player, targeting) - if not success: - return - var ability_cd: float = abilities[i].cooldown - var gcd_cd: float = GCD_TIME if abilities[i].uses_gcd else 0.0 - cooldowns[i] = ability_cd - max_cooldowns[i] = max(ability_cd, gcd_cd) - if abilities[i].uses_gcd: - gcd_timer = GCD_TIME + for i in range(5): + if event.is_action_pressed("ability_%s" % (i + 1)): + EventBus.ability_use_requested.emit(player, i) return - -func apply_passive(base: float, stat: String = "damage") -> float: - for ability in abilities: - if ability and ability.type == Ability.Type.PASSIVE and ability.passive_stat == stat: - return base * (1.0 + ability.damage / 100.0) - return base - -func _on_role_changed(_player: Node, _role_type: int) -> void: - _load_abilities() diff --git a/scripts/player/movement.gd b/scripts/player/movement.gd index 16a3f2a..bc37858 100644 --- a/scripts/player/movement.gd +++ b/scripts/player/movement.gd @@ -1,8 +1,5 @@ extends Node -const SPEED := 5.0 -const JUMP_VELOCITY := 4.5 - var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") @onready var player: CharacterBody3D = get_parent() @@ -11,8 +8,12 @@ func _physics_process(delta: float) -> void: if not player.is_on_floor(): player.velocity.y -= gravity * delta + var base: BaseStats = Stats.get_base(player) + var speed: float = base.speed if base is PlayerStats else 5.0 + var jump_velocity: float = base.jump_velocity if base is PlayerStats else 4.5 + if Input.is_action_just_pressed("ui_accept") and player.is_on_floor(): - player.velocity.y = JUMP_VELOCITY + player.velocity.y = jump_velocity var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back") var camera_pivot := player.get_node("CameraPivot") as Node3D @@ -26,10 +27,10 @@ func _physics_process(delta: float) -> void: var direction := (forward * -input_dir.y + right * input_dir.x).normalized() if direction: - player.velocity.x = direction.x * SPEED - player.velocity.z = direction.z * SPEED + player.velocity.x = direction.x * speed + player.velocity.z = direction.z * speed else: - player.velocity.x = move_toward(player.velocity.x, 0, SPEED) - player.velocity.z = move_toward(player.velocity.z, 0, SPEED) + player.velocity.x = move_toward(player.velocity.x, 0, speed) + player.velocity.z = move_toward(player.velocity.z, 0, speed) player.move_and_slide() diff --git a/scripts/player/player.gd b/scripts/player/player.gd index 32859a8..2367fdf 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -1,8 +1,28 @@ extends CharacterBody3D +@export var stats: BaseStats + func _ready() -> void: add_to_group("player") + Stats.register(self, stats) + var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system") + if cooldown_system: + cooldown_system.register(self, 5) if GameState.returning_from_dungeon: GameState.restore_player(self) global_position = GameState.portal_position + Vector3(0, 1, -5) GameState.returning_from_dungeon = false + elif GameState.dungeon_cleared: + GameState.clear() + var health: float = Stats.get_stat(self, "health") + var max_health: float = Stats.get_stat(self, "max_health") + var shield: float = Stats.get_stat(self, "shield") + var max_shield: float = Stats.get_stat(self, "max_shield") + EventBus.health_changed.emit(self, health, max_health) + EventBus.shield_changed.emit(self, shield, max_shield) + +func _exit_tree() -> void: + Stats.deregister(self) + var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system") + if cooldown_system: + cooldown_system.deregister(self) diff --git a/scripts/player/respawn.gd b/scripts/player/respawn.gd deleted file mode 100644 index c63ac92..0000000 --- a/scripts/player/respawn.gd +++ /dev/null @@ -1,46 +0,0 @@ -extends Node - -const RESPAWN_TIME := 3.0 -var respawn_timer := 0.0 -var is_dead := false -var spawn_position: Vector3 - -@onready var player: CharacterBody3D = get_parent() - -func _ready() -> void: - spawn_position = Vector3(0, 1, -5) - EventBus.entity_died.connect(_on_entity_died) - -func _process(delta: float) -> void: - if is_dead: - respawn_timer -= delta - EventBus.respawn_tick.emit(respawn_timer) - if respawn_timer <= 0: - _respawn() - -func _on_entity_died(entity: Node) -> void: - if entity == player and not is_dead: - is_dead = true - respawn_timer = RESPAWN_TIME - player.velocity = Vector3.ZERO - player.get_node("Mesh").visible = false - player.get_node("CollisionShape3D").disabled = true - player.get_node("Movement").set_physics_process(false) - player.get_node("Combat").set_process_unhandled_input(false) - player.get_node("Targeting").set_process_unhandled_input(false) - -func _respawn() -> void: - is_dead = false - player.global_position = spawn_position - player.get_node("Mesh").visible = true - player.get_node("CollisionShape3D").disabled = false - player.get_node("Movement").set_physics_process(true) - player.get_node("Combat").set_process_unhandled_input(true) - player.get_node("Targeting").set_process_unhandled_input(true) - var health_node: Node = player.get_node("Health") - var shield_node: Node = player.get_node("Shield") - health_node.current_health = health_node.max_health - shield_node.current_shield = shield_node.max_shield - EventBus.health_changed.emit(player, health_node.current_health, health_node.max_health) - EventBus.shield_changed.emit(player, shield_node.current_shield, shield_node.max_shield) - EventBus.player_respawned.emit(player) diff --git a/scripts/player/respawn.gd.uid b/scripts/player/respawn.gd.uid deleted file mode 100644 index 06060cd..0000000 --- a/scripts/player/respawn.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dw3dtax5bx0of diff --git a/scripts/portal/portal.gd b/scripts/portal/portal.gd index 5e548de..ebd3f81 100644 --- a/scripts/portal/portal.gd +++ b/scripts/portal/portal.gd @@ -1,11 +1,17 @@ extends StaticBody3D +@export var stats: BaseStats + const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn") func _ready() -> void: add_to_group("portals") + Stats.register(self, stats) EventBus.entity_died.connect(_on_entity_died) +func _exit_tree() -> void: + Stats.deregister(self) + func _on_entity_died(entity: Node) -> void: if entity != self: return diff --git a/scripts/resources/entity_stats.gd b/scripts/resources/base_stats.gd similarity index 89% rename from scripts/resources/entity_stats.gd rename to scripts/resources/base_stats.gd index acd4ac9..f5d073a 100644 --- a/scripts/resources/entity_stats.gd +++ b/scripts/resources/base_stats.gd @@ -1,5 +1,5 @@ extends Resource -class_name EntityStats +class_name BaseStats @export var max_health := 100.0 @export var health_regen := 0.0 diff --git a/scripts/resources/base_stats.gd.uid b/scripts/resources/base_stats.gd.uid new file mode 100644 index 0000000..b7ecabe --- /dev/null +++ b/scripts/resources/base_stats.gd.uid @@ -0,0 +1 @@ +uid://cet184f878lb8 diff --git a/scripts/resources/boss_stats.gd b/scripts/resources/boss_stats.gd new file mode 100644 index 0000000..b7c8a45 --- /dev/null +++ b/scripts/resources/boss_stats.gd @@ -0,0 +1,2 @@ +extends EnemyStats +class_name BossStats diff --git a/scripts/resources/boss_stats.gd.uid b/scripts/resources/boss_stats.gd.uid new file mode 100644 index 0000000..83957d1 --- /dev/null +++ b/scripts/resources/boss_stats.gd.uid @@ -0,0 +1 @@ +uid://bio01w2gd5e7q diff --git a/scripts/resources/enemy_stats.gd b/scripts/resources/enemy_stats.gd new file mode 100644 index 0000000..d8cfd3e --- /dev/null +++ b/scripts/resources/enemy_stats.gd @@ -0,0 +1,12 @@ +extends BaseStats +class_name EnemyStats + +@export var speed := 3.0 +@export var attack_range := 2.0 +@export var attack_cooldown := 1.5 +@export var attack_damage := 5.0 +@export var regen_fast := 0.10 +@export var regen_slow := 0.01 +@export var aggro_decay := 1.0 +@export var portal_radius := 10.0 +@export var alert_radius := 3.0 diff --git a/scripts/resources/enemy_stats.gd.uid b/scripts/resources/enemy_stats.gd.uid new file mode 100644 index 0000000..d4cd96d --- /dev/null +++ b/scripts/resources/enemy_stats.gd.uid @@ -0,0 +1 @@ +uid://bh2uuuvl30y0x diff --git a/scripts/resources/entity_stats.gd.uid b/scripts/resources/entity_stats.gd.uid deleted file mode 100644 index d9c7bce..0000000 --- a/scripts/resources/entity_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ij663bdj2cgu diff --git a/scripts/resources/player_stats.gd b/scripts/resources/player_stats.gd new file mode 100644 index 0000000..63e7769 --- /dev/null +++ b/scripts/resources/player_stats.gd @@ -0,0 +1,10 @@ +extends BaseStats +class_name PlayerStats + +@export var speed := 5.0 +@export var jump_velocity := 4.5 +@export var target_range := 20.0 +@export var combat_timeout := 3.0 +@export var respawn_time := 3.0 +@export var gcd_time := 0.5 +@export var aa_cooldown := 0.5 diff --git a/scripts/resources/player_stats.gd.uid b/scripts/resources/player_stats.gd.uid new file mode 100644 index 0000000..b768b97 --- /dev/null +++ b/scripts/resources/player_stats.gd.uid @@ -0,0 +1 @@ +uid://ypyntbavbsto diff --git a/scripts/resources/portal_stats.gd b/scripts/resources/portal_stats.gd new file mode 100644 index 0000000..7a41d9f --- /dev/null +++ b/scripts/resources/portal_stats.gd @@ -0,0 +1,5 @@ +extends BaseStats +class_name PortalStats + +@export var spawn_count := 3 +@export var thresholds: Array[float] = [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] diff --git a/scripts/resources/portal_stats.gd.uid b/scripts/resources/portal_stats.gd.uid new file mode 100644 index 0000000..49367e2 --- /dev/null +++ b/scripts/resources/portal_stats.gd.uid @@ -0,0 +1 @@ +uid://bioid3s5oftxs diff --git a/scripts/stats.gd b/scripts/stats.gd new file mode 100644 index 0000000..532f9aa --- /dev/null +++ b/scripts/stats.gd @@ -0,0 +1,53 @@ +extends Node + +var entities: Dictionary = {} +var player_cache: Dictionary = {} + +func register(entity: Node, base: BaseStats) -> void: + if entity.is_in_group("player") and not player_cache.is_empty(): + entities[entity] = player_cache.duplicate() + entities[entity]["base_stats"] = base + player_cache.clear() + else: + entities[entity] = { + "base_stats": base, + "health": base.max_health, + "max_health": base.max_health, + "health_regen": base.health_regen, + "shield": base.max_shield, + "max_shield": base.max_shield, + "shield_regen_delay": base.shield_regen_delay, + "shield_regen_time": base.shield_regen_time, + "shield_regen_timer": 0.0, + "alive": true, + "buff_damage": 1.0, + "buff_heal": 1.0, + "buff_shield": 1.0, + } + +func deregister(entity: Node) -> void: + if entity.is_in_group("player") and entity in entities: + player_cache = entities[entity].duplicate() + entities.erase(entity) + +func clear_player_cache() -> void: + player_cache.clear() + +func get_stat(entity: Node, key: String) -> Variant: + if entity in entities: + return entities[entity].get(key) + return null + +func set_stat(entity: Node, key: String, value: Variant) -> void: + if entity in entities: + entities[entity][key] = value + +func get_base(entity: Node) -> BaseStats: + if entity in entities: + return entities[entity]["base_stats"] + return null + +func is_alive(entity: Node) -> bool: + if entity in entities: + return entities[entity]["alive"] + return false diff --git a/scripts/stats.gd.uid b/scripts/stats.gd.uid new file mode 100644 index 0000000..fd713ad --- /dev/null +++ b/scripts/stats.gd.uid @@ -0,0 +1 @@ +uid://cyxmpeib7pcw7 diff --git a/scripts/systems/ability_system.gd b/scripts/systems/ability_system.gd new file mode 100644 index 0000000..6f89e6e --- /dev/null +++ b/scripts/systems/ability_system.gd @@ -0,0 +1,169 @@ +extends Node + +func _ready() -> void: + EventBus.ability_use_requested.connect(_on_ability_use_requested) + +func _process(_delta: float) -> void: + var players := get_tree().get_nodes_in_group("player") + for player in players: + if not Stats.is_alive(player): + continue + _try_auto_attack(player) + +func _try_auto_attack(player: Node) -> void: + var targeting: Node = player.get_node_or_null("Targeting") + if not targeting or not targeting.in_combat or not targeting.current_target: + return + if not is_instance_valid(targeting.current_target): + return + var cooldown_system: Node = get_node("../CooldownSystem") + if not cooldown_system.is_aa_ready(player): + return + var role: Node = player.get_node("Role") + var ability_set: AbilitySet = role.get_ability_set() + if not ability_set: + return + var aa_damage: float = ability_set.aa_damage + var aa_range: float = ability_set.aa_range + var aa_is_heal: bool = ability_set.aa_is_heal + var dmg: float = _apply_passive(player, aa_damage, "heal" if aa_is_heal else "damage") + if aa_is_heal: + EventBus.heal_requested.emit(player, player, dmg) + else: + var dist: float = player.global_position.distance_to(targeting.current_target.global_position) + if dist > aa_range: + return + EventBus.damage_requested.emit(player, targeting.current_target, dmg) + var base: BaseStats = Stats.get_base(player) + var aa_cd: float = base.aa_cooldown if base is PlayerStats else 0.5 + cooldown_system.set_aa_cooldown(player, aa_cd) + +func _on_ability_use_requested(player: Node, ability_index: int) -> void: + var role: Node = player.get_node_or_null("Role") + if not role: + return + var ability_set: AbilitySet = role.get_ability_set() + if not ability_set or ability_index >= ability_set.abilities.size(): + return + var ability: Ability = ability_set.abilities[ability_index] + if not ability or ability.type == Ability.Type.PASSIVE: + return + var cooldown_system: Node = get_node("../CooldownSystem") + if not cooldown_system.is_ready(player, ability_index): + return + if ability.uses_gcd and not cooldown_system.is_gcd_ready(player): + return + 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 gcd: float = gcd_time if ability.uses_gcd else 0.0 + cooldown_system.set_cooldown(player, ability_index, ability.cooldown, gcd) + +func _execute_ability(player: Node, ability: Ability) -> bool: + var targeting: Node = player.get_node("Targeting") + var stat: String = "heal" if ability.is_heal else "damage" + var dmg: float = _apply_passive(player, ability.damage, stat) + match ability.type: + Ability.Type.SINGLE: + return _execute_single(player, targeting, ability, dmg) + Ability.Type.AOE: + return _execute_aoe(player, ability, dmg) + Ability.Type.UTILITY: + return _execute_utility(player, ability) + Ability.Type.ULT: + return _execute_ult(player, targeting, ability, dmg) + return false + +func _apply_passive(player: Node, base: float, stat: String) -> float: + var mult: Variant = Stats.get_stat(player, "buff_" + stat) + if mult != null: + return base * mult + return base + +func _in_range(player: Node, targeting: Node, ability: Ability) -> bool: + if ability.ability_range <= 0 or ability.is_heal: + return true + if not is_instance_valid(targeting.current_target): + return false + var dist: float = player.global_position.distance_to(targeting.current_target.global_position) + return dist <= ability.ability_range + +func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: + if ability.is_heal: + EventBus.heal_requested.emit(player, player, dmg) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return true + if not _in_range(player, targeting, ability): + return false + if not is_instance_valid(targeting.current_target): + return false + EventBus.damage_requested.emit(player, targeting.current_target, dmg) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return true + +func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: + if ability.is_heal: + EventBus.heal_requested.emit(player, player, dmg) + var players := get_tree().get_nodes_in_group("player") + for p in players: + if p != player and is_instance_valid(p): + var dist: float = player.global_position.distance_to(p.global_position) + if dist <= ability.ability_range: + EventBus.heal_requested.emit(player, p, dmg) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return true + var hit := false + var enemies := get_tree().get_nodes_in_group("enemies") + for enemy in enemies: + var dist: float = player.global_position.distance_to(enemy.global_position) + if dist <= ability.ability_range: + EventBus.damage_requested.emit(player, enemy, dmg) + hit = true + if hit: + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return hit + +func _execute_utility(player: Node, ability: Ability) -> bool: + var max_shield: float = Stats.get_stat(player, "max_shield") + if max_shield <= 0: + return false + var shield: float = Stats.get_stat(player, "shield") + if ability.damage > 0: + shield = max_shield * (ability.damage / 100.0) + else: + if shield >= max_shield: + return false + shield = max_shield + Stats.set_stat(player, "shield", shield) + EventBus.shield_changed.emit(player, shield, max_shield) + return true + +func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool: + if ability.is_heal: + EventBus.heal_requested.emit(player, player, dmg) + var players := get_tree().get_nodes_in_group("player") + var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range + for p in players: + if p != player and is_instance_valid(p): + var dist: float = player.global_position.distance_to(p.global_position) + if dist <= aoe_range: + EventBus.heal_requested.emit(player, p, dmg * 0.4) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return true + if not _in_range(player, targeting, ability): + return false + if not is_instance_valid(targeting.current_target): + 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 + 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: + EventBus.damage_requested.emit(player, enemy, dmg * 2.0) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0) + return true diff --git a/scripts/systems/ability_system.gd.uid b/scripts/systems/ability_system.gd.uid new file mode 100644 index 0000000..e0d7add --- /dev/null +++ b/scripts/systems/ability_system.gd.uid @@ -0,0 +1 @@ +uid://h0hts425epc6 diff --git a/scripts/systems/aggro_system.gd b/scripts/systems/aggro_system.gd new file mode 100644 index 0000000..51e2ec7 --- /dev/null +++ b/scripts/systems/aggro_system.gd @@ -0,0 +1,130 @@ +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/scripts/systems/aggro_system.gd.uid b/scripts/systems/aggro_system.gd.uid new file mode 100644 index 0000000..95f3ef4 --- /dev/null +++ b/scripts/systems/aggro_system.gd.uid @@ -0,0 +1 @@ +uid://cm7ehl2pexcst diff --git a/scripts/systems/buff_system.gd b/scripts/systems/buff_system.gd new file mode 100644 index 0000000..bbd7b57 --- /dev/null +++ b/scripts/systems/buff_system.gd @@ -0,0 +1,37 @@ +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/scripts/systems/buff_system.gd.uid b/scripts/systems/buff_system.gd.uid new file mode 100644 index 0000000..742a992 --- /dev/null +++ b/scripts/systems/buff_system.gd.uid @@ -0,0 +1 @@ +uid://da2jm0awq2lnh diff --git a/scripts/systems/cooldown_system.gd b/scripts/systems/cooldown_system.gd new file mode 100644 index 0000000..44e605e --- /dev/null +++ b/scripts/systems/cooldown_system.gd @@ -0,0 +1,73 @@ +extends Node + +var cooldowns: Dictionary = {} + +func _ready() -> void: + add_to_group("cooldown_system") + EventBus.role_changed.connect(_on_role_changed) + +func register(entity: Node, ability_count: int) -> void: + cooldowns[entity] = { + "cds": [] as Array[float], + "max_cds": [] as Array[float], + "gcd": 0.0, + "aa": 0.0, + } + cooldowns[entity]["cds"].resize(ability_count) + cooldowns[entity]["cds"].fill(0.0) + cooldowns[entity]["max_cds"].resize(ability_count) + cooldowns[entity]["max_cds"].fill(0.0) + +func deregister(entity: Node) -> void: + cooldowns.erase(entity) + +func _process(delta: float) -> void: + for entity in cooldowns: + if not is_instance_valid(entity): + continue + var data: Dictionary = cooldowns[entity] + if data["gcd"] > 0: + data["gcd"] -= delta + if data["aa"] > 0: + data["aa"] -= delta + var cds: Array = data["cds"] + for i in range(cds.size()): + if cds[i] > 0: + cds[i] -= delta + EventBus.cooldown_tick.emit(cds, data["max_cds"], data["gcd"]) + +func is_ready(entity: Node, index: int) -> bool: + if entity not in cooldowns: + return false + return cooldowns[entity]["cds"][index] <= 0 + +func is_gcd_ready(entity: Node) -> bool: + if entity not in cooldowns: + return false + return cooldowns[entity]["gcd"] <= 0 + +func is_aa_ready(entity: Node) -> bool: + if entity not in cooldowns: + return false + return cooldowns[entity]["aa"] <= 0 + +func set_cooldown(entity: Node, index: int, cd: float, gcd: float) -> void: + if entity not in cooldowns: + return + var data: Dictionary = cooldowns[entity] + data["cds"][index] = cd + data["max_cds"][index] = max(cd, gcd) + if gcd > 0: + data["gcd"] = gcd + +func set_aa_cooldown(entity: Node, cd: float) -> void: + if entity not in cooldowns: + return + cooldowns[entity]["aa"] = cd + +func _on_role_changed(player: Node, _role_type: int) -> void: + if player in cooldowns: + var data: Dictionary = cooldowns[player] + data["cds"].fill(0.0) + data["max_cds"].fill(0.0) + data["gcd"] = 0.0 diff --git a/scripts/systems/cooldown_system.gd.uid b/scripts/systems/cooldown_system.gd.uid new file mode 100644 index 0000000..f0728cc --- /dev/null +++ b/scripts/systems/cooldown_system.gd.uid @@ -0,0 +1 @@ +uid://ddos7mo8rahou diff --git a/scripts/systems/damage_system.gd b/scripts/systems/damage_system.gd new file mode 100644 index 0000000..61510e1 --- /dev/null +++ b/scripts/systems/damage_system.gd @@ -0,0 +1 @@ +extends Node diff --git a/scripts/systems/damage_system.gd.uid b/scripts/systems/damage_system.gd.uid new file mode 100644 index 0000000..867c531 --- /dev/null +++ b/scripts/systems/damage_system.gd.uid @@ -0,0 +1 @@ +uid://cbd1bryh0e2dw diff --git a/scripts/systems/enemy_ai_system.gd b/scripts/systems/enemy_ai_system.gd new file mode 100644 index 0000000..3b431f1 --- /dev/null +++ b/scripts/systems/enemy_ai_system.gd @@ -0,0 +1,36 @@ +extends Node + +var attack_timers: Dictionary = {} + +func _physics_process(delta: float) -> void: + for enemy in get_tree().get_nodes_in_group("enemies"): + if not is_instance_valid(enemy) or not Stats.is_alive(enemy): + continue + if enemy.state != enemy.State.ATTACK: + continue + _handle_attack(enemy, delta) + +func _handle_attack(enemy: Node, delta: float) -> void: + if enemy not in attack_timers: + attack_timers[enemy] = 0.0 + attack_timers[enemy] -= delta + + if not is_instance_valid(enemy.target): + enemy.state = enemy.State.RETURN + return + + var base: BaseStats = Stats.get_base(enemy) + var attack_range: float = base.attack_range if base is EnemyStats else 2.0 + var dist: float = enemy.global_position.distance_to(enemy.target.global_position) + if dist > attack_range: + enemy.state = enemy.State.CHASE + return + + if attack_timers[enemy] <= 0: + var attack_cooldown: float = base.attack_cooldown if base is EnemyStats else 1.5 + var attack_damage: float = base.attack_damage if base is EnemyStats else 5.0 + attack_timers[enemy] = attack_cooldown + EventBus.damage_requested.emit(enemy, enemy.target, attack_damage) + + enemy.velocity.x = 0 + enemy.velocity.z = 0 diff --git a/scripts/systems/enemy_ai_system.gd.uid b/scripts/systems/enemy_ai_system.gd.uid new file mode 100644 index 0000000..c4470f9 --- /dev/null +++ b/scripts/systems/enemy_ai_system.gd.uid @@ -0,0 +1 @@ +uid://bwhxu5586lc1l diff --git a/scripts/systems/health_system.gd b/scripts/systems/health_system.gd new file mode 100644 index 0000000..156ea6e --- /dev/null +++ b/scripts/systems/health_system.gd @@ -0,0 +1,49 @@ +extends Node + +func _ready() -> void: + EventBus.damage_requested.connect(_on_damage_requested) + EventBus.heal_requested.connect(_on_heal_requested) + +func _process(delta: float) -> void: + for entity in Stats.entities: + if not is_instance_valid(entity): + continue + var data: Dictionary = Stats.entities[entity] + if not data["alive"]: + continue + var regen: float = data["health_regen"] + if regen > 0 and data["health"] < data["max_health"]: + data["health"] = min(data["health"] + regen * delta, data["max_health"]) + EventBus.health_changed.emit(entity, data["health"], data["max_health"]) + +func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: + if not Stats.is_alive(target): + return + var remaining: float = amount + var shield_system: Node = get_node_or_null("../ShieldSystem") + if shield_system: + remaining = shield_system.absorb(target, remaining) + EventBus.damage_dealt.emit(attacker, target, amount) + if remaining > 0: + _take_damage(target, remaining) + +func _take_damage(entity: Node, amount: float) -> void: + var health: float = Stats.get_stat(entity, "health") + health -= amount + if health <= 0: + health = 0 + Stats.set_stat(entity, "health", health) + var max_health: float = Stats.get_stat(entity, "max_health") + EventBus.health_changed.emit(entity, health, max_health) + if health <= 0: + Stats.set_stat(entity, "alive", false) + EventBus.entity_died.emit(entity) + +func _on_heal_requested(healer: Node, target: Node, amount: float) -> void: + if not Stats.is_alive(target): + return + var health: float = Stats.get_stat(target, "health") + var max_health: float = Stats.get_stat(target, "max_health") + health = min(health + amount, max_health) + Stats.set_stat(target, "health", health) + EventBus.health_changed.emit(target, health, max_health) diff --git a/scripts/systems/health_system.gd.uid b/scripts/systems/health_system.gd.uid new file mode 100644 index 0000000..b86c109 --- /dev/null +++ b/scripts/systems/health_system.gd.uid @@ -0,0 +1 @@ +uid://b3wkn5118dimy diff --git a/scripts/systems/respawn_system.gd b/scripts/systems/respawn_system.gd new file mode 100644 index 0000000..765eb8b --- /dev/null +++ b/scripts/systems/respawn_system.gd @@ -0,0 +1,48 @@ +extends Node + +var dead_players: Dictionary = {} + +func _ready() -> void: + EventBus.entity_died.connect(_on_entity_died) + +func _process(delta: float) -> void: + for player in dead_players.keys(): + if not is_instance_valid(player): + dead_players.erase(player) + continue + dead_players[player] -= delta + EventBus.respawn_tick.emit(dead_players[player]) + if dead_players[player] <= 0: + _respawn(player) + +func _on_entity_died(entity: Node) -> void: + if not entity.is_in_group("player"): + return + if entity in dead_players: + return + var base: BaseStats = Stats.get_base(entity) + var respawn_time: float = base.respawn_time if base is PlayerStats else 3.0 + dead_players[entity] = respawn_time + entity.velocity = Vector3.ZERO + entity.get_node("Mesh").visible = false + entity.get_node("CollisionShape3D").disabled = true + entity.get_node("Movement").set_physics_process(false) + entity.get_node("Combat").set_process_unhandled_input(false) + entity.get_node("Targeting").set_process_unhandled_input(false) + +func _respawn(player: Node) -> void: + dead_players.erase(player) + player.global_position = Vector3(0, 1, -5) + player.get_node("Mesh").visible = true + player.get_node("CollisionShape3D").disabled = false + player.get_node("Movement").set_physics_process(true) + player.get_node("Combat").set_process_unhandled_input(true) + player.get_node("Targeting").set_process_unhandled_input(true) + var max_health: float = Stats.get_stat(player, "max_health") + var max_shield: float = Stats.get_stat(player, "max_shield") + Stats.set_stat(player, "health", max_health) + Stats.set_stat(player, "shield", max_shield) + Stats.set_stat(player, "alive", true) + EventBus.health_changed.emit(player, max_health, max_health) + EventBus.shield_changed.emit(player, max_shield, max_shield) + EventBus.player_respawned.emit(player) diff --git a/scripts/systems/respawn_system.gd.uid b/scripts/systems/respawn_system.gd.uid new file mode 100644 index 0000000..8b9fca0 --- /dev/null +++ b/scripts/systems/respawn_system.gd.uid @@ -0,0 +1 @@ +uid://b1qkvoqvmd21h diff --git a/scripts/systems/shield_system.gd b/scripts/systems/shield_system.gd new file mode 100644 index 0000000..171a1ab --- /dev/null +++ b/scripts/systems/shield_system.gd @@ -0,0 +1,38 @@ +extends Node + +func _process(delta: float) -> void: + for entity in Stats.entities: + if not is_instance_valid(entity): + continue + var data: Dictionary = Stats.entities[entity] + if not data["alive"]: + continue + var max_shield: float = data["max_shield"] + if max_shield <= 0: + continue + var shield: float = data["shield"] + if shield < max_shield: + data["shield_regen_timer"] += delta + if data["shield_regen_timer"] >= data["shield_regen_delay"]: + var regen_rate: float = max_shield / data["shield_regen_time"] + shield += regen_rate * delta + if shield >= max_shield: + shield = max_shield + EventBus.shield_regenerated.emit(entity) + data["shield"] = shield + EventBus.shield_changed.emit(entity, shield, max_shield) + +func absorb(entity: Node, amount: float) -> float: + var shield: float = Stats.get_stat(entity, "shield") + if shield == null or shield <= 0: + return amount + Stats.set_stat(entity, "shield_regen_timer", 0.0) + var absorbed: float = min(amount, shield) + shield -= absorbed + Stats.set_stat(entity, "shield", shield) + var max_shield: float = Stats.get_stat(entity, "max_shield") + if shield <= 0: + EventBus.shield_broken.emit(entity) + EventBus.shield_changed.emit(entity, shield, max_shield) + return amount - absorbed + diff --git a/scripts/systems/shield_system.gd.uid b/scripts/systems/shield_system.gd.uid new file mode 100644 index 0000000..a8ea48c --- /dev/null +++ b/scripts/systems/shield_system.gd.uid @@ -0,0 +1 @@ +uid://rsnpuf77o0sn diff --git a/scripts/systems/spawn_system.gd b/scripts/systems/spawn_system.gd new file mode 100644 index 0000000..0e244f3 --- /dev/null +++ b/scripts/systems/spawn_system.gd @@ -0,0 +1,52 @@ +extends Node + +const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn") + +var portal_data: Dictionary = {} + +func _ready() -> void: + EventBus.health_changed.connect(_on_health_changed) + EventBus.entity_died.connect(_on_entity_died) + +func _on_health_changed(entity: Node, current: float, max_val: float) -> void: + if not entity.is_in_group("portals"): + return + if entity not in portal_data: + var base: BaseStats = Stats.get_base(entity) + var thresholds: Array[float] = base.thresholds if base is PortalStats else [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] + var triggered: Array[bool] = [] + triggered.resize(thresholds.size()) + triggered.fill(false) + portal_data[entity] = { "thresholds": thresholds, "triggered": triggered } + if current <= 0: + return + var data: Dictionary = portal_data[entity] + var ratio: float = current / max_val + var base: BaseStats = Stats.get_base(entity) + var spawn_count: int = base.spawn_count if base is PortalStats else 3 + for i in range(data["thresholds"].size()): + if not data["triggered"][i] and ratio <= data["thresholds"][i]: + data["triggered"][i] = true + _spawn_enemies(entity, spawn_count) + +func _spawn_enemies(portal: Node, count: int) -> void: + var spawned: Array = [] + for j in range(count): + var entity: Node = ENEMY_SCENE.instantiate() + var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) + portal.get_parent().add_child(entity) + entity.global_position = portal.global_position + offset + entity.spawn_position = portal.global_position + entity.portal = portal + spawned.append(entity) + var player: Node = get_tree().get_first_node_in_group("player") + if player: + var dist: float = portal.global_position.distance_to(player.global_position) + if dist <= 10.0: + for entity in spawned: + EventBus.enemy_detected.emit(entity, player) + EventBus.portal_spawn.emit(portal, spawned) + +func _on_entity_died(entity: Node) -> void: + if entity.is_in_group("portals"): + portal_data.erase(entity) diff --git a/scripts/systems/spawn_system.gd.uid b/scripts/systems/spawn_system.gd.uid new file mode 100644 index 0000000..45c450e --- /dev/null +++ b/scripts/systems/spawn_system.gd.uid @@ -0,0 +1 @@ +uid://c84voxmnaifyt diff --git a/scripts/world/portal_spawner.gd b/scripts/world/portal_spawner.gd index 7f22918..94e0095 100644 --- a/scripts/world/portal_spawner.gd +++ b/scripts/world/portal_spawner.gd @@ -39,7 +39,6 @@ func _spawn_portal() -> void: get_parent().add_child(portal) portal.global_position = pos portals.append(portal) - print("Portal gespawnt bei: %s" % pos) func _cleanup_dead() -> void: portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))