update
This commit is contained in:
50
CLAUDE.md
50
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)
|
- Keine Debug-Prints im finalen Code (nur temporär zum Testen)
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
- **Systeme berechnen und entscheiden**, Szenen rendern und senden/empfangen
|
- **Stats (Model)**: Autoload, zentrale Datenhaltung aller Entity-Attribute. Basiswerte aus Resources.
|
||||||
- **Event-Flow**: Input → Intention-Event → System → Ergebnis-Event → Node
|
- **Systeme (Controller)**: Scene-Nodes in world.tscn/dungeon.tscn, lesen/schreiben über Stats.
|
||||||
- **Systeme** sind Scene-Nodes (nicht Autoloads), gefunden über Gruppen
|
- **Szenen (Views)**: Rendern, Input senden, Events empfangen. Kein Gameplay-State.
|
||||||
- **Zwischen Szenen**: Kommunikation über EventBus (Autoload). Szenen kennen sich nicht.
|
- **EventBus (Signals)**: Autoload, Kommunikation zwischen Szenen und Systemen.
|
||||||
- **Innerhalb einer Szene**: Modulare Skripte als Child-Nodes, Zugriff auf Geschwister-Nodes erlaubt.
|
- **Event-Flow**: Szene → Input → EventBus → System → Stats → EventBus → Szene
|
||||||
- **Autoloads**: EventBus (Signals), GameState (Spielerzustand zwischen Szenenwechseln)
|
- **Zwischen Szenen**: Kommunikation über EventBus. Szenen kennen sich nicht.
|
||||||
- **Gruppen**: "player", "enemies", "portals", "boss"
|
- **Innerhalb einer Szene**: Zugriff auf Geschwister-Nodes erlaubt.
|
||||||
- **Resources** für statische Konfiguration (Stats, Abilities), **Nodes** für laufenden Zustand
|
- **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
|
## Projektstruktur
|
||||||
- `scenes/` — .tscn Dateien
|
- `scenes/` — .tscn Dateien
|
||||||
- `world.tscn` — Hauptszene (100x100m, Taverne in Mitte)
|
- `world.tscn` — Hauptszene (100x100m, Taverne, 10 Systeme)
|
||||||
- `player/player.tscn` — Spieler
|
- `player/player.tscn` — Spieler
|
||||||
- `enemy/enemy.tscn` — Gegner
|
- `enemy/enemy.tscn` — Gegner
|
||||||
- `enemy/boss.tscn` — Boss (eigene Szene, erbt von Enemy)
|
- `enemy/boss.tscn` — Boss
|
||||||
- `portal/portal.tscn` — Portal (Gegner-Spawner)
|
- `portal/portal.tscn` — Portal
|
||||||
- `portal/gate.tscn` — Gate (Teleporter, konfigurierbar: Dungeon-Eingang oder Exit)
|
- `portal/gate.tscn` — Gate (Teleporter)
|
||||||
- `dungeon/dungeon.tscn` — Dungeon (15x90m Schlauch, 4 Gegnergruppen + Boss)
|
- `dungeon/dungeon.tscn` — Dungeon (15x90m, 10 Systeme)
|
||||||
- `hud/hud.tscn` — HUD
|
- `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/player/` — Spieler-Skripte (player, camera, movement, combat, targeting, role, hud)
|
||||||
- `scripts/enemy/` — Gegner-Skripte (enemy, enemy_movement, boss)
|
- `scripts/enemy/` — Gegner-Skripte (enemy, enemy_movement, boss)
|
||||||
- `scripts/portal/` — Portal + Gate (portal, gate)
|
- `scripts/portal/` — Portal + Gate (portal, gate)
|
||||||
- `scripts/dungeon/` — Dungeon-Logik (dungeon_manager)
|
- `scripts/dungeon/` — Dungeon-Logik (dungeon_manager)
|
||||||
- `scripts/components/` — Wiederverwendbare Komponenten (health, shield, healthbar)
|
- `scripts/components/` — Healthbar (healthbar)
|
||||||
- `scripts/abilities/` — Ability-System (ability, ability_set)
|
- `scripts/abilities/` — Ability-Daten (ability, ability_set)
|
||||||
- `scripts/resources/` — Resource-Klassen (entity_stats)
|
- `scripts/resources/` — Resource-Klassen (base_stats, player_stats, enemy_stats, boss_stats, portal_stats)
|
||||||
- `scripts/event_bus.gd` — Globale Signals
|
- `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/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)
|
- `resources/ability_sets/` — AbilitySet .tres pro Rolle (tank_set, damage_set, healer_set)
|
||||||
|
|
||||||
## Planungsdokument
|
## 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
|
- Heiler: Heilt statt schadet (Single, AOE, Ult), Heal-Passive, AOE macht Schaden
|
||||||
|
|
||||||
## Szenenwechsel
|
## 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 (Eingang): save_player → Dungeon laden
|
||||||
- Gate (Exit): save_player, returning_from_dungeon → Welt laden, Spieler bei Gate-Position
|
- Gate (Exit): returning_from_dungeon → Welt laden, Spieler bei Gate-Position
|
||||||
- Boss-Tod: dungeon_cleared → Welt laden, Spieler bei Taverne, Gates weg
|
- Boss-Tod: dungeon_cleared → Welt laden, Cache geleert, Spieler bei Taverne mit vollen HP
|
||||||
- PortalSpawner stellt Gate wieder her wenn portal_position gesetzt und Boss noch lebt
|
|
||||||
|
|
||||||
## Workflow mit dem User
|
## Workflow mit dem User
|
||||||
- **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren
|
- **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren
|
||||||
|
|||||||
74
plan.md
74
plan.md
@@ -29,17 +29,12 @@
|
|||||||
- event_bus.gd — Autoload-Singleton, globale Signals
|
- event_bus.gd — Autoload-Singleton, globale Signals
|
||||||
- Intentionen (Input → System):
|
- Intentionen (Input → System):
|
||||||
- ability_use_requested(player, ability_index) — Spieler will Ability nutzen
|
- 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
|
- 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:
|
- Kampf:
|
||||||
- attack_executed(attacker, position, direction, damage) — Angriff wurde ausgeführt
|
- attack_executed(attacker, position, direction, damage) — Angriff wurde ausgeführt
|
||||||
- damage_dealt(attacker, target, damage) — Schaden wurde verteilt
|
- 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:
|
||||||
- entity_died(entity) — Entity ist gestorben
|
- entity_died(entity) — Entity ist gestorben
|
||||||
- health_changed(entity, current, max) — Leben hat sich verändert
|
- 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
|
- cooldown_tick(cooldowns, max_cooldowns, gcd_timer) — Cooldown-Update für HUD
|
||||||
- Buff:
|
- Buff:
|
||||||
- buff_changed(entity, stat, value) — Buff hat sich verändert
|
- buff_changed(entity, stat, value) — Buff hat sich verändert
|
||||||
- Regeneration:
|
|
||||||
- regeneration_changed(entity, current, max) — Regeneration hat sich verändert
|
|
||||||
- Gegner:
|
- Gegner:
|
||||||
- enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert
|
- enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert
|
||||||
- Portal:
|
- Portal:
|
||||||
@@ -63,6 +56,8 @@
|
|||||||
- portal_defeated(portal) — Portal besiegt, wird Gate
|
- portal_defeated(portal) — Portal besiegt, wird Gate
|
||||||
- Dungeon:
|
- Dungeon:
|
||||||
- dungeon_cleared() — Boss tot, Dungeon gesäubert
|
- 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
|
# Resources
|
||||||
## BaseStats
|
## BaseStats
|
||||||
@@ -89,59 +84,53 @@
|
|||||||
- Verändert Ability (Element, Beruf, Prestige)
|
- Verändert Ability (Element, Beruf, Prestige)
|
||||||
|
|
||||||
# Systeme
|
# Systeme
|
||||||
- Werden in der Hauptszene instanziert
|
- In jeder Root-Szene instanziert (world.tscn, dungeon.tscn)
|
||||||
- Entities registrieren/deregistrieren sich bei Stats
|
- Entities registrieren/deregistrieren sich bei Stats
|
||||||
- Systeme lesen/schreiben über Stats
|
- Systeme lesen/schreiben über Stats
|
||||||
### HealthSystem (health_system.gd)
|
### HealthSystem (health_system.gd)
|
||||||
- Leben (health) und Lebensregeneration (regeneration) berechnen
|
- Leben und Lebensregeneration berechnen, Tod bei 0 HP
|
||||||
- Tod bei 0 HP
|
|
||||||
- Listener: damage_requested, heal_requested
|
- Listener: damage_requested, heal_requested
|
||||||
- Event: health_changed, regeneration_changed, entity_died
|
- Event: health_changed, entity_died
|
||||||
### ShieldSystem (shield_system.gd)
|
### ShieldSystem (shield_system.gd)
|
||||||
- Schild (shield) und Schildregeneration (shield_regeneration) berechnen
|
- Schild und Schildregeneration berechnen
|
||||||
- Wie zweite Lebensleiste
|
- absorb(entity, amount) → remaining damage
|
||||||
- Event: shield_changed, shield_broken, shield_regenerated
|
- Event: shield_changed, shield_broken, shield_regenerated
|
||||||
### RespawnSystem (respawn_system.gd)
|
### RespawnSystem (respawn_system.gd)
|
||||||
- Respawn bei Taverne mit vollen Leben und Schild
|
- Respawn bei Taverne mit vollen Leben und Schild
|
||||||
- Tod-Timer (3s)
|
|
||||||
- Listener: entity_died
|
- Listener: entity_died
|
||||||
- Event: respawn_tick, player_respawned
|
- Event: respawn_tick, player_respawned
|
||||||
### AbilitySystem (ability_system.gd)
|
### AbilitySystem (ability_system.gd)
|
||||||
- Single, AOE, Utility, Ult
|
- Ability-Ausführung (Single, AOE, Utility, Ult) + Auto-Attack in _process
|
||||||
- Auto-Attack
|
- Listener: ability_use_requested
|
||||||
- Listener: ability_use_requested, auto_attack_tick
|
- Event: attack_executed, damage_requested, heal_requested
|
||||||
- Event: attack_executed
|
### BuffSystem (buff_system.gd)
|
||||||
## BuffSystem (buff_system.gd)
|
- Passive-Buffs (damage/heal/shield Multiplikatoren in Stats)
|
||||||
- Passive
|
|
||||||
- Listener: role_changed
|
- Listener: role_changed
|
||||||
- Event: buff_changed
|
- Event: buff_changed, shield_changed
|
||||||
### CooldownSystem (cooldown_system.gd)
|
### CooldownSystem (cooldown_system.gd)
|
||||||
- Cooldown-Tracking, GCD
|
- Cooldown-Tracking, GCD, AA-Timer per Entity
|
||||||
- Listener: attack_executed
|
- register/deregister per Entity, direkte Funktionsaufrufe vom AbilitySystem
|
||||||
- Event: cooldown_tick
|
- Event: cooldown_tick
|
||||||
### DamageSystem (damage_system.gd)
|
### DamageSystem (damage_system.gd)
|
||||||
- Schadensberechnung
|
- Reserviert für spätere Schadensberechnung (aktuell leer)
|
||||||
- Listener: attack_executed
|
|
||||||
- Event: damage_requested, heal_requested
|
|
||||||
### AggroSystem (aggro_system.gd)
|
### AggroSystem (aggro_system.gd)
|
||||||
- Aggro-Tabellen, Decay, Zielwahl, Kampfstatus, Nearby-Alerting
|
- Aggro-Tabellen, Decay, Zielwahl, Nearby-Alerting
|
||||||
- Listener: damage_dealt, heal_requested, entity_died, enemy_detected, target_requested
|
- Listener: damage_dealt, heal_requested, entity_died, enemy_detected
|
||||||
- Event: target_changed, combat_state_changed, enemy_target_changed, enemy_engaged
|
- Event: enemy_engaged
|
||||||
### EnemyAISystem (enemy_ai_system.gd)
|
### EnemyAISystem (enemy_ai_system.gd)
|
||||||
- Gegner-States (Idle/Chase/Attack/Return), Bewegungsbefehle
|
- ATTACK-State: Range-Check, Timer, Schaden
|
||||||
- Listener: enemy_target_changed, entity_died
|
- Iteriert Enemies in _physics_process
|
||||||
- Event: enemy_state_changed
|
- Event: damage_requested
|
||||||
### SpawnSystem (spawn_system.gd)
|
### SpawnSystem (spawn_system.gd)
|
||||||
- Entity-Erstellung, Portal/Gate-Spawning, Dungeon-Clear
|
- Portal-HP-Schwellen-Spawning
|
||||||
- Export: mode ("world" / "dungeon")
|
- Listener: health_changed, entity_died
|
||||||
- Listener: entity_died, health_changed
|
- Event: portal_spawn
|
||||||
- Event: portal_spawn, portal_defeated, dungeon_cleared
|
|
||||||
|
|
||||||
# Szenen
|
# Szenen
|
||||||
- Szenen sind Views — rendern, Input senden, Events empfangen
|
- Szenen sind Views — rendern, Input senden, Events empfangen
|
||||||
- Kein Gameplay-State in Szenen (liegt in Stats Autoload)
|
- Kein Gameplay-State in Szenen (liegt in Stats Autoload)
|
||||||
- Entities registrieren sich bei Stats in _ready(), deregistrieren in _exit_tree()
|
- 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
|
## Welt
|
||||||
- world.tscn — Hauptszene (100x100m)
|
- world.tscn — Hauptszene (100x100m)
|
||||||
@@ -163,7 +152,7 @@
|
|||||||
- CameraPivot (Node3D, camera.gd)
|
- CameraPivot (Node3D, camera.gd)
|
||||||
- Camera3D
|
- Camera3D
|
||||||
- Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats
|
- 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
|
- Role (Node, role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed
|
||||||
- Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested
|
- Targeting (Node, targeting.gd) — Klick/TAB, emittiert target_requested
|
||||||
- player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn
|
- player.gd — Registriert bei Stats mit PlayerStats Resource, Sichtbarkeit bei Tod/Respawn
|
||||||
@@ -215,13 +204,16 @@
|
|||||||
|
|
||||||
## Dungeon
|
## Dungeon
|
||||||
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
|
- dungeon.tscn — Geschlossener Raum (15x90m, Wände, dunkles Licht)
|
||||||
|
- Systems (alle 10 Systeme, temporär bis Welt parallel läuft)
|
||||||
- NavigationRegion3D
|
- NavigationRegion3D
|
||||||
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
|
- Boden, 4 Wände (StaticBody3D + BoxMesh, 3m hoch)
|
||||||
|
- Spieler (Instanz von player.tscn)
|
||||||
|
- HUD (Instanz von hud.tscn)
|
||||||
- Gegnergruppen (4x4 Gegner)
|
- Gegnergruppen (4x4 Gegner)
|
||||||
- Boss (Instanz von boss.tscn)
|
- Boss (Instanz von boss.tscn)
|
||||||
- Exit-Gate (Instanz von gate.tscn, is_exit=true)
|
- Exit-Gate (Instanz von gate.tscn, is_exit=true)
|
||||||
- DungeonManager (Node, dungeon_manager.gd)
|
- 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
|
||||||
- hud.tscn — CanvasLayer
|
- hud.tscn — CanvasLayer
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ config/icon="res://icon.svg"
|
|||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
EventBus="*res://scripts/event_bus.gd"
|
EventBus="*res://scripts/event_bus.gd"
|
||||||
|
Stats="*res://scripts/stats.gd"
|
||||||
GameState="*res://scripts/game_state.gd"
|
GameState="*res://scripts/game_state.gd"
|
||||||
|
|
||||||
[dotnet]
|
[dotnet]
|
||||||
|
|||||||
@@ -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]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
@@ -9,3 +9,12 @@ health_regen = 0.0
|
|||||||
max_shield = 100.0
|
max_shield = 100.0
|
||||||
shield_regen_delay = 5.0
|
shield_regen_delay = 5.0
|
||||||
shield_regen_time = 8.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
|
||||||
|
|||||||
@@ -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]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
max_health = 100.0
|
|
||||||
health_regen = 0.0
|
|
||||||
max_shield = 50.0
|
max_shield = 50.0
|
||||||
shield_regen_delay = 3.0
|
|
||||||
shield_regen_time = 5.0
|
|
||||||
|
|||||||
@@ -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]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
max_health = 100.0
|
|
||||||
health_regen = 1.0
|
health_regen = 1.0
|
||||||
max_shield = 50.0
|
max_shield = 50.0
|
||||||
shield_regen_delay = 3.0
|
|
||||||
shield_regen_time = 5.0
|
|
||||||
|
|||||||
@@ -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]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
max_health = 500.0
|
max_health = 500.0
|
||||||
health_regen = 0.0
|
|
||||||
max_shield = 0.0
|
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
[ext_resource type="PackedScene" path="res://scenes/enemy/boss.tscn" id="boss"]
|
[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="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="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"]
|
[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)
|
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="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="."]
|
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
|
||||||
navigation_mesh = SubResource("NavigationMesh_1")
|
navigation_mesh = SubResource("NavigationMesh_1")
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
[gd_scene load_steps=6 format=3]
|
[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/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/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_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"]
|
[ext_resource type="Resource" path="res://resources/stats/boss_stats.tres" id="8"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
[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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
|
||||||
bg_color = Color(0.2, 0.8, 0.2, 1)
|
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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
|
||||||
bg_color = Color(0.1, 0.1, 0.3, 1)
|
bg_color = Color(0.1, 0.1, 0.3, 1)
|
||||||
|
|
||||||
@@ -45,6 +38,7 @@ radius = 8.0
|
|||||||
|
|
||||||
[node name="Boss" type="CharacterBody3D"]
|
[node name="Boss" type="CharacterBody3D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
stats = ExtResource("8")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
shape = SubResource("CapsuleShape3D_1")
|
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)
|
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0)
|
||||||
mesh = SubResource("SphereMesh_1")
|
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="."]
|
[node name="HitArea" type="Area3D" parent="."]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
@@ -73,12 +59,6 @@ shape = SubResource("CapsuleShape3D_2")
|
|||||||
[node name="EnemyMovement" type="Node" parent="."]
|
[node name="EnemyMovement" type="Node" parent="."]
|
||||||
script = ExtResource("5")
|
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="."]
|
[node name="DetectionArea" type="Area3D" parent="."]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
[gd_scene load_steps=6 format=3]
|
[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/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/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_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"]
|
[ext_resource type="Resource" path="res://resources/stats/enemy_stats.tres" id="8"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
[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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
|
||||||
bg_color = Color(0.2, 0.8, 0.2, 1)
|
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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
|
||||||
bg_color = Color(0.1, 0.1, 0.3, 1)
|
bg_color = Color(0.1, 0.1, 0.3, 1)
|
||||||
|
|
||||||
@@ -45,6 +38,7 @@ radius = 8.0
|
|||||||
|
|
||||||
[node name="Enemy" type="CharacterBody3D"]
|
[node name="Enemy" type="CharacterBody3D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
stats = ExtResource("8")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
shape = SubResource("CapsuleShape3D_1")
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
@@ -52,14 +46,6 @@ shape = SubResource("CapsuleShape3D_1")
|
|||||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||||
mesh = SubResource("SphereMesh_1")
|
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="."]
|
[node name="HitArea" type="Area3D" parent="."]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
@@ -72,12 +58,6 @@ shape = SubResource("CapsuleShape3D_2")
|
|||||||
[node name="EnemyMovement" type="Node" parent="."]
|
[node name="EnemyMovement" type="Node" parent="."]
|
||||||
script = ExtResource("5")
|
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="."]
|
[node name="DetectionArea" type="Area3D" parent="."]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
|
|||||||
@@ -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://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://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://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://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="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://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"]
|
[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]
|
[node name="Player" type="CharacterBody3D" unique_id=1350215040]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
stats = ExtResource("14")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=33887999]
|
||||||
shape = SubResource("CapsuleShape3D_1")
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
@@ -47,17 +45,6 @@ script = ExtResource("4")
|
|||||||
[node name="Targeting" type="Node" parent="." unique_id=592540710]
|
[node name="Targeting" type="Node" parent="." unique_id=592540710]
|
||||||
script = ExtResource("8")
|
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]
|
[node name="Role" type="Node" parent="." unique_id=134158295]
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
tank_set = ExtResource("11")
|
tank_set = ExtResource("11")
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
[gd_scene format=3]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/portal/portal.gd" id="1"]
|
[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/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"]
|
[ext_resource type="Resource" path="res://resources/stats/portal_stats.tres" id="6"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
|
[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"]
|
[node name="Portal" type="StaticBody3D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
stats = ExtResource("6")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
shape = SubResource("CylinderShape3D_1")
|
shape = SubResource("CylinderShape3D_1")
|
||||||
@@ -42,10 +40,6 @@ shape = SubResource("CylinderShape3D_1")
|
|||||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||||
mesh = SubResource("CylinderMesh_1")
|
mesh = SubResource("CylinderMesh_1")
|
||||||
|
|
||||||
[node name="Health" type="Node" parent="."]
|
|
||||||
script = ExtResource("2")
|
|
||||||
stats = ExtResource("6")
|
|
||||||
|
|
||||||
[node name="HitArea" type="Area3D" parent="."]
|
[node name="HitArea" type="Area3D" parent="."]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
@@ -61,10 +55,6 @@ monitoring = true
|
|||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"]
|
||||||
shape = SubResource("SphereShape3D_1")
|
shape = SubResource("SphereShape3D_1")
|
||||||
|
|
||||||
[node name="Spawner" type="Node" parent="."]
|
|
||||||
script = ExtResource("4")
|
|
||||||
spawn_scene = ExtResource("5")
|
|
||||||
|
|
||||||
[node name="Healthbar" type="Sprite3D" parent="."]
|
[node name="Healthbar" type="Sprite3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
|
||||||
billboard = 1
|
billboard = 1
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
[gd_scene format=3 uid="uid://dy1icabu2ssbw"]
|
[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" 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="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://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"]
|
[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)
|
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="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]
|
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=1265843679]
|
||||||
navigation_mesh = SubResource("NavigationMesh_1")
|
navigation_mesh = SubResource("NavigationMesh_1")
|
||||||
|
|
||||||
|
|||||||
@@ -13,107 +13,3 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
|
|||||||
@export var icon: String = ""
|
@export var icon: String = ""
|
||||||
@export var is_heal: bool = false
|
@export var is_heal: bool = false
|
||||||
@export var passive_stat: String = "damage"
|
@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
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://b053b4fkkeaod
|
|
||||||
@@ -3,38 +3,55 @@ extends Sprite3D
|
|||||||
@onready var viewport: SubViewport = $SubViewport
|
@onready var viewport: SubViewport = $SubViewport
|
||||||
@onready var health_bar: ProgressBar = $SubViewport/HealthBar
|
@onready var health_bar: ProgressBar = $SubViewport/HealthBar
|
||||||
@onready var border: ColorRect = $SubViewport/Border
|
@onready var border: ColorRect = $SubViewport/Border
|
||||||
@onready var health: Node = get_parent().get_node("Health")
|
|
||||||
@onready var parent_node: Node = get_parent()
|
@onready var parent_node: Node = get_parent()
|
||||||
|
|
||||||
var shield: Node = null
|
|
||||||
var shield_bar: ProgressBar = null
|
var shield_bar: ProgressBar = null
|
||||||
var style_normal: StyleBoxFlat
|
var style_normal: StyleBoxFlat
|
||||||
var style_aggro: StyleBoxFlat
|
var style_aggro: StyleBoxFlat
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
texture = viewport.get_texture()
|
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")
|
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
|
border.visible = false
|
||||||
style_normal = health_bar.get_theme_stylebox("fill").duplicate()
|
style_normal = health_bar.get_theme_stylebox("fill").duplicate()
|
||||||
style_aggro = style_normal.duplicate()
|
style_aggro = style_normal.duplicate()
|
||||||
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
|
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
|
||||||
EventBus.target_changed.connect(_on_target_changed)
|
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:
|
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")
|
var player: Node = get_tree().get_first_node_in_group("player")
|
||||||
if player and "target" in parent_node and parent_node.target == player:
|
if player and "target" in parent_node and parent_node.target == player:
|
||||||
health_bar.add_theme_stylebox_override("fill", style_aggro)
|
health_bar.add_theme_stylebox_override("fill", style_aggro)
|
||||||
else:
|
else:
|
||||||
health_bar.add_theme_stylebox_override("fill", style_normal)
|
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:
|
func _on_target_changed(_player: Node, target: Node) -> void:
|
||||||
border.visible = (target == get_parent())
|
border.visible = (target == get_parent())
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bpfw71oprcvou
|
|
||||||
@@ -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)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cm2s3xkmuesey
|
|
||||||
@@ -11,6 +11,6 @@ func _on_entity_died(entity: Node) -> void:
|
|||||||
await get_tree().create_timer(2.0).timeout
|
await get_tree().create_timer(2.0).timeout
|
||||||
GameState.dungeon_cleared = true
|
GameState.dungeon_cleared = true
|
||||||
GameState.returning_from_dungeon = false
|
GameState.returning_from_dungeon = false
|
||||||
GameState.clear_player()
|
GameState.clear()
|
||||||
EventBus.dungeon_cleared.emit()
|
EventBus.dungeon_cleared.emit()
|
||||||
get_tree().change_scene_to_file("res://scenes/world.tscn")
|
get_tree().change_scene_to_file("res://scenes/world.tscn")
|
||||||
|
|||||||
@@ -2,56 +2,35 @@ extends CharacterBody3D
|
|||||||
|
|
||||||
enum State { IDLE, CHASE, ATTACK, RETURN }
|
enum State { IDLE, CHASE, ATTACK, RETURN }
|
||||||
|
|
||||||
|
@export var stats: BaseStats
|
||||||
|
|
||||||
var state: int = State.IDLE
|
var state: int = State.IDLE
|
||||||
var target: Node3D = null
|
var target: Node3D = null
|
||||||
var spawn_position: Vector3
|
var spawn_position: Vector3
|
||||||
var portal: Node3D = null
|
var portal: Node3D = null
|
||||||
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||||
|
|
||||||
@onready var health: Node = $Health
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
spawn_position = global_position
|
spawn_position = global_position
|
||||||
add_to_group("enemies")
|
add_to_group("enemies")
|
||||||
|
Stats.register(self, stats)
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
EventBus.entity_died.connect(_on_entity_died)
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
Stats.deregister(self)
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
func _on_entity_died(entity: Node) -> void:
|
||||||
if entity == self:
|
if entity == self:
|
||||||
queue_free()
|
queue_free()
|
||||||
elif entity == target:
|
|
||||||
target = null
|
|
||||||
state = State.RETURN
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
velocity.y -= gravity * delta
|
velocity.y -= gravity * delta
|
||||||
move_and_slide()
|
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:
|
func _on_detection_area_body_entered(body: Node3D) -> void:
|
||||||
if body is CharacterBody3D and body.name == "Player":
|
if body is CharacterBody3D and body.name == "Player":
|
||||||
_engage(body)
|
EventBus.enemy_detected.emit(self, body)
|
||||||
EventBus.enemy_engaged.emit(self, body)
|
|
||||||
|
|
||||||
func _on_detection_area_body_exited(body: Node3D) -> void:
|
func _on_detection_area_body_exited(_body: Node3D) -> void:
|
||||||
if body == target and state == State.CHASE:
|
pass
|
||||||
state = State.RETURN
|
|
||||||
target = 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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bojdohjxr6uef
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ct4u62xalrjyo
|
|
||||||
@@ -49,10 +49,14 @@ func _return_to_spawn(delta: float) -> void:
|
|||||||
_regenerate(delta)
|
_regenerate(delta)
|
||||||
|
|
||||||
func _regenerate(delta: float) -> void:
|
func _regenerate(delta: float) -> void:
|
||||||
var health: Node = enemy.get_node("Health")
|
var health: float = Stats.get_stat(enemy, "health")
|
||||||
if health.current_health < health.max_health:
|
var max_health: float = Stats.get_stat(enemy, "max_health")
|
||||||
var rate: float = REGEN_FAST if health.current_health < health.max_health else REGEN_SLOW
|
if health == null or max_health == null:
|
||||||
if health.current_health >= health.max_health * 0.99:
|
return
|
||||||
|
if health < max_health:
|
||||||
|
var rate: float = REGEN_FAST
|
||||||
|
if health >= max_health * 0.99:
|
||||||
rate = REGEN_SLOW
|
rate = REGEN_SLOW
|
||||||
health.current_health = min(health.current_health + health.max_health * rate * delta, health.max_health)
|
health = min(health + max_health * rate * delta, max_health)
|
||||||
EventBus.health_changed.emit(enemy, health.current_health, health.max_health)
|
Stats.set_stat(enemy, "health", health)
|
||||||
|
EventBus.health_changed.emit(enemy, health, max_health)
|
||||||
|
|||||||
@@ -1,20 +1,46 @@
|
|||||||
extends Node
|
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 attack_executed(attacker, position, direction, damage)
|
||||||
signal damage_dealt(attacker, target, 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 entity_died(entity)
|
||||||
|
signal health_changed(entity, current, max_val)
|
||||||
|
signal shield_changed(entity, current, max_val)
|
||||||
signal shield_broken(entity)
|
signal shield_broken(entity)
|
||||||
signal shield_regenerated(entity)
|
signal shield_regenerated(entity)
|
||||||
|
signal regeneration_changed(entity, current, max_val)
|
||||||
|
|
||||||
|
# Spieler
|
||||||
signal target_changed(player, target)
|
signal target_changed(player, target)
|
||||||
signal player_respawned(player)
|
signal player_respawned(player)
|
||||||
signal role_changed(player, role_type)
|
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 respawn_tick(timer)
|
||||||
signal enemy_engaged(enemy, target)
|
|
||||||
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
|
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 portal_spawn(portal, enemies)
|
||||||
signal heal_requested(healer, target, amount)
|
|
||||||
signal portal_defeated(portal)
|
signal portal_defeated(portal)
|
||||||
|
|
||||||
|
# Dungeon
|
||||||
signal dungeon_cleared()
|
signal dungeon_cleared()
|
||||||
|
|||||||
@@ -1,42 +1,20 @@
|
|||||||
extends Node
|
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 player_role: int = 1
|
||||||
var portal_position: Vector3 = Vector3.ZERO
|
var portal_position: Vector3 = Vector3.ZERO
|
||||||
var returning_from_dungeon := false
|
var returning_from_dungeon := false
|
||||||
var dungeon_cleared := false
|
var dungeon_cleared := false
|
||||||
|
|
||||||
func save_player(player: Node) -> void:
|
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")
|
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
|
player_role = role.current_role
|
||||||
|
|
||||||
func restore_player(player: Node) -> void:
|
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")
|
var role: Node = player.get_node("Role")
|
||||||
health.current_health = player_health
|
|
||||||
shield.current_shield = player_shield
|
|
||||||
role.set_role(player_role)
|
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:
|
func clear() -> void:
|
||||||
clear_player()
|
Stats.clear_player_cache()
|
||||||
portal_position = Vector3.ZERO
|
portal_position = Vector3.ZERO
|
||||||
returning_from_dungeon = false
|
returning_from_dungeon = false
|
||||||
dungeon_cleared = false
|
dungeon_cleared = false
|
||||||
|
|||||||
@@ -1,93 +1,9 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const GCD_TIME := 0.5
|
|
||||||
const AA_COOLDOWN := 0.5
|
|
||||||
|
|
||||||
@onready var player: CharacterBody3D = get_parent()
|
@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:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
for i in range(min(abilities.size(), 5)):
|
for i in range(5):
|
||||||
if event.is_action_pressed("ability_%s" % (i + 1)) and abilities[i]:
|
if event.is_action_pressed("ability_%s" % (i + 1)):
|
||||||
if abilities[i].type == Ability.Type.PASSIVE:
|
EventBus.ability_use_requested.emit(player, i)
|
||||||
return
|
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
|
|
||||||
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()
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const SPEED := 5.0
|
|
||||||
const JUMP_VELOCITY := 4.5
|
|
||||||
|
|
||||||
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||||
|
|
||||||
@onready var player: CharacterBody3D = get_parent()
|
@onready var player: CharacterBody3D = get_parent()
|
||||||
@@ -11,8 +8,12 @@ func _physics_process(delta: float) -> void:
|
|||||||
if not player.is_on_floor():
|
if not player.is_on_floor():
|
||||||
player.velocity.y -= gravity * delta
|
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():
|
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 input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||||
var camera_pivot := player.get_node("CameraPivot") as Node3D
|
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()
|
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
|
||||||
|
|
||||||
if direction:
|
if direction:
|
||||||
player.velocity.x = direction.x * SPEED
|
player.velocity.x = direction.x * speed
|
||||||
player.velocity.z = direction.z * SPEED
|
player.velocity.z = direction.z * speed
|
||||||
else:
|
else:
|
||||||
player.velocity.x = move_toward(player.velocity.x, 0, SPEED)
|
player.velocity.x = move_toward(player.velocity.x, 0, speed)
|
||||||
player.velocity.z = move_toward(player.velocity.z, 0, SPEED)
|
player.velocity.z = move_toward(player.velocity.z, 0, speed)
|
||||||
|
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var stats: BaseStats
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("player")
|
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:
|
if GameState.returning_from_dungeon:
|
||||||
GameState.restore_player(self)
|
GameState.restore_player(self)
|
||||||
global_position = GameState.portal_position + Vector3(0, 1, -5)
|
global_position = GameState.portal_position + Vector3(0, 1, -5)
|
||||||
GameState.returning_from_dungeon = false
|
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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dw3dtax5bx0of
|
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
extends StaticBody3D
|
extends StaticBody3D
|
||||||
|
|
||||||
|
@export var stats: BaseStats
|
||||||
|
|
||||||
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
|
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("portals")
|
add_to_group("portals")
|
||||||
|
Stats.register(self, stats)
|
||||||
EventBus.entity_died.connect(_on_entity_died)
|
EventBus.entity_died.connect(_on_entity_died)
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
Stats.deregister(self)
|
||||||
|
|
||||||
func _on_entity_died(entity: Node) -> void:
|
func _on_entity_died(entity: Node) -> void:
|
||||||
if entity != self:
|
if entity != self:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
extends Resource
|
extends Resource
|
||||||
class_name EntityStats
|
class_name BaseStats
|
||||||
|
|
||||||
@export var max_health := 100.0
|
@export var max_health := 100.0
|
||||||
@export var health_regen := 0.0
|
@export var health_regen := 0.0
|
||||||
1
scripts/resources/base_stats.gd.uid
Normal file
1
scripts/resources/base_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cet184f878lb8
|
||||||
2
scripts/resources/boss_stats.gd
Normal file
2
scripts/resources/boss_stats.gd
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
extends EnemyStats
|
||||||
|
class_name BossStats
|
||||||
1
scripts/resources/boss_stats.gd.uid
Normal file
1
scripts/resources/boss_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bio01w2gd5e7q
|
||||||
12
scripts/resources/enemy_stats.gd
Normal file
12
scripts/resources/enemy_stats.gd
Normal file
@@ -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
|
||||||
1
scripts/resources/enemy_stats.gd.uid
Normal file
1
scripts/resources/enemy_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bh2uuuvl30y0x
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ij663bdj2cgu
|
|
||||||
10
scripts/resources/player_stats.gd
Normal file
10
scripts/resources/player_stats.gd
Normal file
@@ -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
|
||||||
1
scripts/resources/player_stats.gd.uid
Normal file
1
scripts/resources/player_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ypyntbavbsto
|
||||||
5
scripts/resources/portal_stats.gd
Normal file
5
scripts/resources/portal_stats.gd
Normal file
@@ -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]
|
||||||
1
scripts/resources/portal_stats.gd.uid
Normal file
1
scripts/resources/portal_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bioid3s5oftxs
|
||||||
53
scripts/stats.gd
Normal file
53
scripts/stats.gd
Normal file
@@ -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
|
||||||
1
scripts/stats.gd.uid
Normal file
1
scripts/stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cyxmpeib7pcw7
|
||||||
169
scripts/systems/ability_system.gd
Normal file
169
scripts/systems/ability_system.gd
Normal file
@@ -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
|
||||||
1
scripts/systems/ability_system.gd.uid
Normal file
1
scripts/systems/ability_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://h0hts425epc6
|
||||||
130
scripts/systems/aggro_system.gd
Normal file
130
scripts/systems/aggro_system.gd
Normal file
@@ -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
|
||||||
1
scripts/systems/aggro_system.gd.uid
Normal file
1
scripts/systems/aggro_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cm7ehl2pexcst
|
||||||
37
scripts/systems/buff_system.gd
Normal file
37
scripts/systems/buff_system.gd
Normal file
@@ -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)
|
||||||
1
scripts/systems/buff_system.gd.uid
Normal file
1
scripts/systems/buff_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://da2jm0awq2lnh
|
||||||
73
scripts/systems/cooldown_system.gd
Normal file
73
scripts/systems/cooldown_system.gd
Normal file
@@ -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
|
||||||
1
scripts/systems/cooldown_system.gd.uid
Normal file
1
scripts/systems/cooldown_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ddos7mo8rahou
|
||||||
1
scripts/systems/damage_system.gd
Normal file
1
scripts/systems/damage_system.gd
Normal file
@@ -0,0 +1 @@
|
|||||||
|
extends Node
|
||||||
1
scripts/systems/damage_system.gd.uid
Normal file
1
scripts/systems/damage_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cbd1bryh0e2dw
|
||||||
36
scripts/systems/enemy_ai_system.gd
Normal file
36
scripts/systems/enemy_ai_system.gd
Normal file
@@ -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
|
||||||
1
scripts/systems/enemy_ai_system.gd.uid
Normal file
1
scripts/systems/enemy_ai_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bwhxu5586lc1l
|
||||||
49
scripts/systems/health_system.gd
Normal file
49
scripts/systems/health_system.gd
Normal file
@@ -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)
|
||||||
1
scripts/systems/health_system.gd.uid
Normal file
1
scripts/systems/health_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b3wkn5118dimy
|
||||||
48
scripts/systems/respawn_system.gd
Normal file
48
scripts/systems/respawn_system.gd
Normal file
@@ -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)
|
||||||
1
scripts/systems/respawn_system.gd.uid
Normal file
1
scripts/systems/respawn_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b1qkvoqvmd21h
|
||||||
38
scripts/systems/shield_system.gd
Normal file
38
scripts/systems/shield_system.gd
Normal file
@@ -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
|
||||||
|
|
||||||
1
scripts/systems/shield_system.gd.uid
Normal file
1
scripts/systems/shield_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://rsnpuf77o0sn
|
||||||
52
scripts/systems/spawn_system.gd
Normal file
52
scripts/systems/spawn_system.gd
Normal file
@@ -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)
|
||||||
1
scripts/systems/spawn_system.gd.uid
Normal file
1
scripts/systems/spawn_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c84voxmnaifyt
|
||||||
@@ -39,7 +39,6 @@ func _spawn_portal() -> void:
|
|||||||
get_parent().add_child(portal)
|
get_parent().add_child(portal)
|
||||||
portal.global_position = pos
|
portal.global_position = pos
|
||||||
portals.append(portal)
|
portals.append(portal)
|
||||||
print("Portal gespawnt bei: %s" % pos)
|
|
||||||
|
|
||||||
func _cleanup_dead() -> void:
|
func _cleanup_dead() -> void:
|
||||||
portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))
|
portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))
|
||||||
|
|||||||
Reference in New Issue
Block a user