From 4fddc74df14ffdb3c55346c8f812d391642ab5d1 Mon Sep 17 00:00:00 2001 From: Marek Le Date: Mon, 30 Mar 2026 09:03:29 +0200 Subject: [PATCH] update --- plan.md | 87 ++++++++++++++++---------- resources/abilities/aoe_attack.tres | 2 +- resources/abilities/single_attack.tres | 3 +- resources/abilities/ult_burst.tres | 3 +- resources/stats/enemy_stats.tres | 11 ++++ resources/stats/player_stats.tres | 11 ++++ resources/stats/portal_stats.tres | 9 +++ scenes/enemy/enemy.tscn | 3 + scenes/player/player.tscn | 7 ++- scenes/portal/portal.tscn | 22 ++++++- scenes/world.tscn | 4 +- scripts/abilities/ability.gd | 8 ++- scripts/components/health.gd | 11 ++-- scripts/components/shield.gd | 17 +++-- scripts/components/spawner.gd | 44 +++++++++++++ scripts/components/spawner.gd.uid | 1 + scripts/enemy/enemy.gd | 1 - scripts/enemy/enemy_aggro.gd | 9 ++- scripts/event_bus.gd | 2 +- scripts/player/combat.gd | 29 +++++++-- scripts/player/hud.gd | 6 +- scripts/player/player_class.gd | 37 ----------- scripts/player/player_class.gd.uid | 1 - scripts/player/role.gd | 37 +++++++++++ scripts/player/role.gd.uid | 1 + scripts/player/targeting.gd | 31 ++++----- scripts/portal/portal.gd | 11 +++- scripts/portal/portal_spawner.gd | 30 --------- scripts/portal/portal_spawner.gd.uid | 1 - scripts/resources/entity_stats.gd | 8 +++ scripts/resources/entity_stats.gd.uid | 1 + 31 files changed, 295 insertions(+), 153 deletions(-) create mode 100644 resources/stats/enemy_stats.tres create mode 100644 resources/stats/player_stats.tres create mode 100644 resources/stats/portal_stats.tres create mode 100644 scripts/components/spawner.gd create mode 100644 scripts/components/spawner.gd.uid delete mode 100644 scripts/player/player_class.gd delete mode 100644 scripts/player/player_class.gd.uid create mode 100644 scripts/player/role.gd create mode 100644 scripts/player/role.gd.uid delete mode 100644 scripts/portal/portal_spawner.gd delete mode 100644 scripts/portal/portal_spawner.gd.uid create mode 100644 scripts/resources/entity_stats.gd create mode 100644 scripts/resources/entity_stats.gd.uid diff --git a/plan.md b/plan.md index 68089e8..668481f 100644 --- a/plan.md +++ b/plan.md @@ -21,6 +21,7 @@ - HUD (Instanz von hud.tscn) - player.tscn — Spieler + - Gruppe (player) - CharacterBody3D (player.gd) - Kollision (CapsuleShape3D, 1.8m x 0.3m) - Mesh (CapsuleMesh) @@ -28,7 +29,7 @@ - Camera3D (hinter/über Spieler) - Movement (Node, movement.gd) - Combat (Node, combat.gd) - - PlayerClass (Node, player_class.gd) + - Role (Node, role.gd) - Targeting (Node, targeting.gd) - Health (Node, health.gd) - Shield (Node, shield.gd) @@ -40,23 +41,28 @@ - ShieldBar (ProgressBar, links oben, unter HealthBar) - RespawnTimer (Label, mitte, Countdown bei Tod) - AbilityBar (HBoxContainer, mitte unten) - - ClassIcon (Label, Klassen-Symbol: T=Tank, D=Schaden, H=Heiler) - - Ability 1-5 + - RoleIcon (Label, T=Tank, D=Schaden, H=Heiler) + - Abilities(Label, Fähigkeiten 1-4, Passive P) -- portal.tscn — Portal (Gegner-Spawner, anvisierbar, Gruppe "targetable") +- portal.tscn — Portal (Gegner-Spawner, anvisierbar) + - Gruppe (portals) - StaticBody3D (portal.gd) - Kollision (CylinderShape3D) - - Mesh (CylinderMesh, Platzhalter) + - Mesh (CylinderMesh, blau) - Health (Node, health.gd) - HitArea (Area3D, Trefferbereich) - CollisionShape3D - - PortalSpawner (Node, portal_spawner.gd) - - Spawnt 3 Gegner bei 85%, 70%, 55%, 40%, 25%, 10% Leben (einmalig) - - Portal = Spawnpunkt der Gegner - - 10m Aggro-Radius um das Portal + - Spawner (Node, spawner.gd) + - DetectionArea (Area3D, 10m Radius, Auto-Targeting bei Betreten) + - CollisionShape3D (SphereShape3D) - Healthbar (Sprite3D + SubViewport, healthbar.gd) + - Phase 1: Angreifbar, HP-Bar, spawnt 3 Gegner bei 85%/70%/55%/40%/25%/10% Leben + - Portal = Spawnpunkt, 10m Aggro-Radius + - Phase 2 (geplant): Bei 0 HP → Portal wird Gate, teleportiert in Dungeon + - Abgesperrter Bereich mit Gegner-Gruppen und Boss - enemy.tscn — Gegner + - Gruppe (enemies) - CharacterBody3D (enemy.gd) - Kollision (CapsuleShape3D) - Mesh (SphereMesh, Platzhalter) @@ -80,41 +86,54 @@ - player.gd — Kern, verbindet Komponenten - camera.gd — LMB freies Umsehen, RMB Kamera + Laufrichtung anpassen - movement.gd — Bewegung (WASD relativ zur Kamera), Springen, Schwerkraft -- combat.gd — Führt Abilities aus, verwaltet Cooldowns (GCD 1s), Fähigkeiten 1-5 - - 1 Single: 10 Schaden, Distanzprüfung, 0s CD, GCD - - 2 AOE: 5 Schaden im Bereich, 3s CD, GCD - - 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD - - 4 Ult: 4x Single + 2x AOE um Ziel, 30s CD, GCD - - 5 Passive: 50% mehr Schaden (permanent aktiv, kein CD) -- targeting.gd — Klick/TAB anvisieren (Gruppe "targetable"), Kampfmodus bei Gegner-Angriff, Auto-Targeting auf nächsten Gegner +- combat.gd — Führt Abilities aus, verwaltet Cooldowns (GCD 0.5s), Auto-Attack (10 Schaden, 1s, 20m) +- targeting.gd — Klick/TAB anvisieren (Gruppen "enemies" + "portals"), Kampfmodus bei Gegner-Angriff, Auto-Targeting (Gegner > Portal) - event_bus.gd — Autoload-Singleton, globale Signals -- portal.gd — Portal-Kern, Gruppe "targetable", kein Kampf/Aggro/State -- portal_spawner.gd — Spawnt Gegner bei Lebensschwellen, setzt Portal als Spawnpunkt -- enemy.gd — Gegner-Kern, State Machine (Idle, Verfolgen, Angreifen, Zurückkehren), alarmiert Gegner in 3m, Gruppe "enemies" + "targetable" +- role.gd — Rollenwechsel ALT+1 bis ALT+3 (Tank/Schaden/Heiler), tauscht AbilitySet +- respawn.gd — Bei Tod: Spieler deaktivieren, 3s Timer, Respawn am Startpunkt, Leben/Schild voll +- hud.gd — Reagiert auf Events, aktualisiert HealthBar/ShieldBar/AbilityBar/RespawnTimer +- portal.gd — Portal-Kern, Gruppe "portals", stirbt bei 0 HP, kein Kampf/Aggro/State +- enemy.gd — Gegner-Kern, State Machine (Idle, Verfolgen, Angreifen, Zurückkehren), alarmiert Gegner in 3m, Gruppe "enemies" - enemy_movement.gd — Navigation zum Ziel/Spawnpunkt - enemy_combat.gd — Angriff über Event (damage_requested) - enemy_aggro.gd — Aggro-Tabelle (Spieler → Wert), wählt Ziel mit höchstem Aggro - - Schaden = Aggro (1:1) - - Heilung = Aggro (0.5x) - - Tank = Aggro-Multiplikator (2x) - - Aggro verfällt -1/s - - Außerhalb 10m Portal-Radius: Aggro verfällt 1% * 2 je s (1%, 2%, 4%, 8%, ...) - - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert 10% Leben/s bis 100%, dann 1%/s - - Bei Spieler-Tod → Aggro auf 0 -- health.gd — Leben, 1/s Regeneration, Tod bei 0 (wiederverwendbar) -- shield.gd — Schild, regeneriert nach 3s ohne Schaden, in 5s voll (wiederverwendbar) -- player_class.gd — Klassenwechsel ALT+1 bis ALT+3 (Tank/Schaden/Heiler), tauscht AbilitySet -- respawn.gd — Bei Tod: Spieler deaktivieren, 3s Timer, Respawn am Startpunkt, Leben/Schild voll -- hud.gd — Reagiert auf Events, aktualisiert HealthBar/ShieldBar/AbilityBar/RespawnTimer + +## Components (scripts/components/, wiederverwendbar) +- health.gd — Leben, Regeneration, Tod bei 0 (liest Base-Werte aus EntityStats) +- shield.gd — Schild, Regeneration nach Delay (liest Base-Werte aus EntityStats) - healthbar.gd — Liest Health/Shield (optional) vom Parent, gelber Rand bei Anvisierung, blauer Lebensbalken wenn Spieler Ziel ist (nur bei Gegnern) +- spawner.gd — Spawnt Entities bei Lebensschwellen, engagt Spieler im Portal-Radius sofort + +## Stats (Resources) +- entity_stats.gd (Resource) — max_health, health_regen, max_shield, shield_regen_delay, shield_regen_time +- player_stats.tres — health=100, regen=1/s, shield=50, delay=3s, regen_time=5s +- enemy_stats.tres — health=100, regen=0, shield=50, delay=3s, regen_time=5s +- portal_stats.tres — health=500, regen=0, shield=0 + +## Aggro +- Schaden = Aggro (1:1) +- Heilung = Aggro (0.5x) +- Tank = Aggro-Multiplikator (2x) +- Aggro verfällt -1/s +- Spieler im Portal-Radius: Aggro bleibt bei mindestens 1 +- Außerhalb 10m Portal-Radius: Aggro verfällt 1% * 2 je s (1%, 2%, 4%, 8%, ...) +- Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert 10% Leben/s bis 100%, dann 1%/s +- Bei Spieler-Tod → Aggro auf 0 ## Abilities (Resources) - ability.gd (Resource) — name, type, damage, range, cooldown, uses_gcd, execute() - ability_set.gd (Resource) — Set von 5 Abilities pro Klasse - ability_modifier.gd (Resource) — Verändert Ability (Element, Beruf, Prestige) - Typen: Single, AOE, Utility, Ult, Passive -- Jede Klasse hat ein eigenes AbilitySet -- Beim Klassenwechsel wird das AbilitySet getauscht +- Auto-Attack: 10 Schaden, 1s, automatisch bei anvisiertem Gegner im Kampf +- Schadens-Klasse: + - 1 Single: 30 Schaden, 20m Range, 1s CD, GCD + - 2 AOE: 20 Schaden, 5m Range, 3s CD, GCD + - 3 Utility: Schild sofort auf 100%, 5s CD, kein GCD + - 4 Ult: 5x Single (50 Schaden) + 2x AOE 3m (20 Schaden), 20m Range, 15s CD, GCD + - 5 Passive: 50% mehr Schaden (permanent aktiv, kein CD) +- Jede Rolle hat ein eigenes AbilitySet +- Beim Rollenwechsel wird das AbilitySet getauscht - Elemente und Modifikatoren verändern Abilities nachträglich ## Events @@ -126,7 +145,7 @@ - shield_regenerated(entity) — Schild ist wieder voll - target_changed(player, target) — Spieler hat neues Ziel anvisiert - player_respawned(player) — Spieler ist respawnt -- class_changed(player, class_name) — Spieler hat Klasse gewechselt +- role_changed(player, role_type) — Spieler hat Rolle gewechselt - health_changed(entity, current, max) — Leben hat sich verändert - shield_changed(entity, current, max) — Schild hat sich verändert - respawn_tick(timer) — Respawn-Countdown Update diff --git a/resources/abilities/aoe_attack.tres b/resources/abilities/aoe_attack.tres index e14c163..192b7ab 100644 --- a/resources/abilities/aoe_attack.tres +++ b/resources/abilities/aoe_attack.tres @@ -6,7 +6,7 @@ script = ExtResource("1") ability_name = "AOE" type = 1 -damage = 5.0 +damage = 20.0 ability_range = 5.0 cooldown = 3.0 icon = "2" diff --git a/resources/abilities/single_attack.tres b/resources/abilities/single_attack.tres index 547d673..d066242 100644 --- a/resources/abilities/single_attack.tres +++ b/resources/abilities/single_attack.tres @@ -5,6 +5,7 @@ [resource] script = ExtResource("1") ability_name = "Single" -damage = 10.0 +damage = 30.0 ability_range = 20.0 +cooldown = 1.0 icon = "1" diff --git a/resources/abilities/ult_burst.tres b/resources/abilities/ult_burst.tres index 9a16570..3994bac 100644 --- a/resources/abilities/ult_burst.tres +++ b/resources/abilities/ult_burst.tres @@ -8,5 +8,6 @@ ability_name = "Burst" type = 3 damage = 10.0 ability_range = 20.0 -cooldown = 30.0 +cooldown = 15.0 +aoe_radius = 3.0 icon = "4" diff --git a/resources/stats/enemy_stats.tres b/resources/stats/enemy_stats.tres new file mode 100644 index 0000000..acdccd4 --- /dev/null +++ b/resources/stats/enemy_stats.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +max_health = 100.0 +health_regen = 0.0 +max_shield = 50.0 +shield_regen_delay = 3.0 +shield_regen_time = 5.0 diff --git a/resources/stats/player_stats.tres b/resources/stats/player_stats.tres new file mode 100644 index 0000000..f3b2589 --- /dev/null +++ b/resources/stats/player_stats.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +max_health = 100.0 +health_regen = 1.0 +max_shield = 50.0 +shield_regen_delay = 3.0 +shield_regen_time = 5.0 diff --git a/resources/stats/portal_stats.tres b/resources/stats/portal_stats.tres new file mode 100644 index 0000000..a07ef45 --- /dev/null +++ b/resources/stats/portal_stats.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="EntityStats" load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/resources/entity_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +max_health = 500.0 +health_regen = 0.0 +max_shield = 0.0 diff --git a/scenes/enemy/enemy.tscn b/scenes/enemy/enemy.tscn index 763a532..832fcbb 100644 --- a/scenes/enemy/enemy.tscn +++ b/scenes/enemy/enemy.tscn @@ -7,6 +7,7 @@ [ext_resource type="Script" path="res://scripts/enemy/enemy_movement.gd" id="5"] [ext_resource type="Script" path="res://scripts/enemy/enemy_combat.gd" id="6"] [ext_resource type="Script" path="res://scripts/enemy/enemy_aggro.gd" id="7"] +[ext_resource type="Resource" path="res://resources/stats/enemy_stats.tres" id="8"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] radius = 0.4 @@ -53,9 +54,11 @@ mesh = SubResource("SphereMesh_1") [node name="Health" type="Node" parent="."] script = ExtResource("2") +stats = ExtResource("8") [node name="Shield" type="Node" parent="."] script = ExtResource("3") +stats = ExtResource("8") [node name="HitArea" type="Area3D" parent="."] collision_layer = 4 diff --git a/scenes/player/player.tscn b/scenes/player/player.tscn index 92c55b6..a817de2 100644 --- a/scenes/player/player.tscn +++ b/scenes/player/player.tscn @@ -8,10 +8,11 @@ [ext_resource type="Script" uid="uid://bpfw71oprcvou" path="res://scripts/components/shield.gd" id="6"] [ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scripts/player/targeting.gd" id="8"] [ext_resource type="Script" uid="uid://dw3dtax5bx0of" path="res://scripts/player/respawn.gd" id="9"] -[ext_resource type="Script" uid="uid://rus4umqvvqq4" path="res://scripts/player/player_class.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://beodknb6i1pm4" path="res://resources/ability_sets/damage_set.tres" id="12"] [ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://resources/ability_sets/healer_set.tres" id="13"] +[ext_resource type="Resource" path="res://resources/stats/player_stats.tres" id="14"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] radius = 0.3 @@ -48,14 +49,16 @@ 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="PlayerClass" type="Node" parent="." unique_id=134158295] +[node name="Role" type="Node" parent="." unique_id=134158295] script = ExtResource("10") tank_set = ExtResource("11") damage_set = ExtResource("12") diff --git a/scenes/portal/portal.tscn b/scenes/portal/portal.tscn index 04dbff9..1ff207a 100644 --- a/scenes/portal/portal.tscn +++ b/scenes/portal/portal.tscn @@ -3,7 +3,12 @@ [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/portal/portal_spawner.gd" id="4"] +[ext_resource type="Script" path="res://scripts/components/spawner.gd" id="4"] +[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="5"] +[ext_resource type="Resource" path="res://resources/stats/portal_stats.tres" id="6"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 10.0 [sub_resource type="CylinderShape3D" id="CylinderShape3D_1"] radius = 1.0 @@ -39,7 +44,7 @@ mesh = SubResource("CylinderMesh_1") [node name="Health" type="Node" parent="."] script = ExtResource("2") -max_health = 500.0 +stats = ExtResource("6") [node name="HitArea" type="Area3D" parent="."] collision_layer = 4 @@ -48,8 +53,17 @@ collision_mask = 0 [node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"] shape = SubResource("CylinderShape3D_2") -[node name="PortalSpawner" type="Node" parent="."] +[node name="DetectionArea" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 1 +monitoring = true + +[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"] +shape = SubResource("SphereShape3D_1") + +[node name="Spawner" type="Node" parent="."] script = ExtResource("4") +spawn_scene = ExtResource("5") [node name="Healthbar" type="Sprite3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) @@ -76,3 +90,5 @@ theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") max_value = 500.0 value = 500.0 show_percentage = false + +[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"] diff --git a/scenes/world.tscn b/scenes/world.tscn index c3b8d8d..5af9edb 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -1,8 +1,8 @@ [gd_scene format=3 uid="uid://dy1icabu2ssbw"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] -[ext_resource type="PackedScene" path="res://scenes/portal/portal.tscn" id="portal"] [ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] +[ext_resource type="PackedScene" path="res://scenes/portal/portal.tscn" id="portal"] [sub_resource type="NavigationMesh" id="NavigationMesh_1"] vertices = PackedVector3Array(-9.5, 0.5, -9.5, -9.5, 0.5, 9.5, 9.5, 0.5, 9.5, 9.5, 0.5, -9.5) @@ -51,5 +51,5 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, 1, -7) [node name="HUD" parent="." unique_id=24362518 instance=ExtResource("hud")] -[node name="Portal" parent="." instance=ExtResource("portal")] +[node name="Portal" parent="." unique_id=2086501116 instance=ExtResource("portal")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5) diff --git a/scripts/abilities/ability.gd b/scripts/abilities/ability.gd index f20e0e9..4275f4a 100644 --- a/scripts/abilities/ability.gd +++ b/scripts/abilities/ability.gd @@ -9,6 +9,7 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } @export var ability_range: float = 3.0 @export var cooldown: float = 0.0 @export var uses_gcd: bool = true +@export var aoe_radius: float = 0.0 @export var icon: String = "" func execute(player: Node, targeting: Node) -> bool: @@ -69,12 +70,13 @@ func _execute_ult(player: Node, targeting: Node, dmg: float) -> bool: if not _in_range(player, targeting): return false var target: Node3D = targeting.current_target - EventBus.damage_requested.emit(player, target, dmg * 4.0) + 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: var enemy_dist: float = target.global_position.distance_to(enemy.global_position) - if enemy_dist <= ability_range: + 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 * 4.0) + EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0) return true diff --git a/scripts/components/health.gd b/scripts/components/health.gd index dbe7429..62721d8 100644 --- a/scripts/components/health.gd +++ b/scripts/components/health.gd @@ -1,16 +1,19 @@ extends Node -@export var max_health := 100.0 -const HEALTH_REGEN := 1.0 +@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) func _process(delta: float) -> void: - if current_health > 0 and current_health < max_health: - current_health = min(current_health + HEALTH_REGEN * delta, max_health) + 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: diff --git a/scripts/components/shield.gd b/scripts/components/shield.gd index 570a573..2b55453 100644 --- a/scripts/components/shield.gd +++ b/scripts/components/shield.gd @@ -1,19 +1,25 @@ extends Node -@export var max_shield := 50.0 -const REGEN_DELAY := 3.0 -const REGEN_TIME := 5.0 +@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 func _ready() -> void: + max_shield = stats.max_shield + regen_delay = stats.shield_regen_delay + regen_time = stats.shield_regen_time current_shield = max_shield 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 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()) @@ -25,7 +31,6 @@ func absorb(amount: float) -> float: regen_timer = 0.0 var absorbed: float = min(amount, current_shield) current_shield -= absorbed - print("%s Schild: %s/%s (-%s)" % [get_parent().name, current_shield, max_shield, absorbed]) if current_shield <= 0: EventBus.shield_broken.emit(get_parent()) EventBus.shield_changed.emit(get_parent(), current_shield, max_shield) diff --git a/scripts/components/spawner.gd b/scripts/components/spawner.gd new file mode 100644 index 0000000..f44d734 --- /dev/null +++ b/scripts/components/spawner.gd @@ -0,0 +1,44 @@ +extends Node + +@export var spawn_scene: PackedScene +@export var spawn_count := 3 +@export var thresholds: Array[float] = [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] + +var triggered: Array[bool] = [] + +@onready var parent: Node3D = get_parent() +@onready var health: Node = get_parent().get_node("Health") + +func _ready() -> void: + triggered.resize(thresholds.size()) + triggered.fill(false) + +func _process(_delta: float) -> void: + if not spawn_scene or health.current_health <= 0: + return + var ratio: float = health.current_health / health.max_health + for i in range(thresholds.size()): + if not triggered[i] and ratio <= thresholds[i]: + triggered[i] = true + _spawn() + +func _spawn() -> void: + var spawned: Array = [] + for j in range(spawn_count): + var entity: Node = spawn_scene.instantiate() + var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) + parent.get_parent().add_child(entity) + entity.global_position = parent.global_position + offset + if "spawn_position" in entity: + entity.spawn_position = parent.global_position + if "portal" in entity: + entity.portal = parent + spawned.append(entity) + var player: Node = get_tree().get_first_node_in_group("player") + if player: + var dist: float = parent.global_position.distance_to(player.global_position) + if dist <= 10.0: + for entity in spawned: + if entity.has_method("_engage"): + entity._engage(player) + EventBus.portal_spawn.emit(parent, spawned) diff --git a/scripts/components/spawner.gd.uid b/scripts/components/spawner.gd.uid new file mode 100644 index 0000000..1e2c351 --- /dev/null +++ b/scripts/components/spawner.gd.uid @@ -0,0 +1 @@ +uid://cm2s3xkmuesey diff --git a/scripts/enemy/enemy.gd b/scripts/enemy/enemy.gd index 11520a9..a71bd55 100644 --- a/scripts/enemy/enemy.gd +++ b/scripts/enemy/enemy.gd @@ -13,7 +13,6 @@ var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") func _ready() -> void: spawn_position = global_position add_to_group("enemies") - add_to_group("targetable") EventBus.entity_died.connect(_on_entity_died) func _on_entity_died(entity: Node) -> void: diff --git a/scripts/enemy/enemy_aggro.gd b/scripts/enemy/enemy_aggro.gd index 4b18132..2f1160c 100644 --- a/scripts/enemy/enemy_aggro.gd +++ b/scripts/enemy/enemy_aggro.gd @@ -27,6 +27,11 @@ func _process(delta: float) -> void: 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) @@ -43,8 +48,8 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: if target != enemy: return var multiplier := 1.0 - var player_class: Node = attacker.get_node_or_null("PlayerClass") - if player_class and player_class.current_class == 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) diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index a742dde..5c45ed6 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -7,7 +7,7 @@ signal shield_broken(entity) signal shield_regenerated(entity) signal target_changed(player, target) signal player_respawned(player) -signal class_changed(player, class_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) diff --git a/scripts/player/combat.gd b/scripts/player/combat.gd index e9e6002..f735d80 100644 --- a/scripts/player/combat.gd +++ b/scripts/player/combat.gd @@ -1,19 +1,23 @@ extends Node const GCD_TIME := 0.5 +const AA_DAMAGE := 10.0 +const AA_COOLDOWN := 1.0 +const AA_RANGE := 20.0 @onready var player: CharacterBody3D = get_parent() @onready var targeting: Node = get_parent().get_node("Targeting") -@onready var player_class: Node = get_parent().get_node("PlayerClass") +@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.class_changed.connect(_on_class_changed) + EventBus.role_changed.connect(_on_role_changed) func _process(delta: float) -> void: if gcd_timer > 0: @@ -22,9 +26,26 @@ func _process(delta: float) -> void: 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 dist := player.global_position.distance_to(targeting.current_target.global_position) + if dist > AA_RANGE: + return + var dmg := apply_passive(AA_DAMAGE) + EventBus.damage_requested.emit(player, targeting.current_target, dmg) + print("AA: %s Schaden an %s" % [dmg, targeting.current_target.name]) + aa_timer = AA_COOLDOWN func _load_abilities() -> void: - var ability_set: AbilitySet = player_class.get_ability_set() + var ability_set: AbilitySet = role.get_ability_set() if ability_set: abilities = ability_set.abilities else: @@ -59,5 +80,5 @@ func apply_passive(base_damage: float) -> float: return base_damage * (1.0 + ability.damage / 100.0) return base_damage -func _on_class_changed(_player: Node, _class_type: int) -> void: +func _on_role_changed(_player: Node, _role_type: int) -> void: _load_abilities() diff --git a/scripts/player/hud.gd b/scripts/player/hud.gd index b646579..2a9d20e 100644 --- a/scripts/player/hud.gd +++ b/scripts/player/hud.gd @@ -22,7 +22,7 @@ func _ready() -> void: EventBus.shield_changed.connect(_on_shield_changed) EventBus.entity_died.connect(_on_entity_died) EventBus.player_respawned.connect(_on_player_respawned) - EventBus.class_changed.connect(_on_class_changed) + EventBus.role_changed.connect(_on_role_changed) EventBus.respawn_tick.connect(_on_respawn_tick) EventBus.cooldown_tick.connect(_on_cooldown_tick) @@ -46,8 +46,8 @@ func _on_player_respawned(_player: Node) -> void: func _on_respawn_tick(timer: float) -> void: respawn_label.text = str(ceil(timer)) -func _on_class_changed(_player: Node, class_type: int) -> void: - match class_type: +func _on_role_changed(_player: Node, role_type: int) -> void: + match role_type: 0: class_icon.text = "T" 1: class_icon.text = "D" 2: class_icon.text = "H" diff --git a/scripts/player/player_class.gd b/scripts/player/player_class.gd deleted file mode 100644 index 955799d..0000000 --- a/scripts/player/player_class.gd +++ /dev/null @@ -1,37 +0,0 @@ -extends Node - -enum PlayerClass { TANK, DAMAGE, HEALER } - -var current_class: int = PlayerClass.DAMAGE - -@export var tank_set: AbilitySet -@export var damage_set: AbilitySet -@export var healer_set: AbilitySet - -@onready var player: CharacterBody3D = get_parent() - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("class_tank"): - set_class(PlayerClass.TANK) - elif event.is_action_pressed("class_damage"): - set_class(PlayerClass.DAMAGE) - elif event.is_action_pressed("class_healer"): - set_class(PlayerClass.HEALER) - -func set_class(new_class: int) -> void: - current_class = new_class - EventBus.class_changed.emit(player, current_class) - -func get_class_icon() -> String: - match current_class: - PlayerClass.TANK: return "T" - PlayerClass.DAMAGE: return "D" - PlayerClass.HEALER: return "H" - return "" - -func get_ability_set() -> AbilitySet: - match current_class: - PlayerClass.TANK: return tank_set - PlayerClass.DAMAGE: return damage_set - PlayerClass.HEALER: return healer_set - return damage_set diff --git a/scripts/player/player_class.gd.uid b/scripts/player/player_class.gd.uid deleted file mode 100644 index fada3de..0000000 --- a/scripts/player/player_class.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://rus4umqvvqq4 diff --git a/scripts/player/role.gd b/scripts/player/role.gd new file mode 100644 index 0000000..e9c89f3 --- /dev/null +++ b/scripts/player/role.gd @@ -0,0 +1,37 @@ +extends Node + +enum Role { TANK, DAMAGE, HEALER } + +var current_role: int = Role.DAMAGE + +@export var tank_set: AbilitySet +@export var damage_set: AbilitySet +@export var healer_set: AbilitySet + +@onready var player: CharacterBody3D = get_parent() + +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("class_tank"): + set_role(Role.TANK) + elif event.is_action_pressed("class_damage"): + set_role(Role.DAMAGE) + elif event.is_action_pressed("class_healer"): + set_role(Role.HEALER) + +func set_role(new_role: int) -> void: + current_role = new_role + EventBus.role_changed.emit(player, current_role) + +func get_role_icon() -> String: + match current_role: + Role.TANK: return "T" + Role.DAMAGE: return "D" + Role.HEALER: return "H" + return "" + +func get_ability_set() -> AbilitySet: + match current_role: + Role.TANK: return tank_set + Role.DAMAGE: return damage_set + Role.HEALER: return healer_set + return damage_set diff --git a/scripts/player/role.gd.uid b/scripts/player/role.gd.uid new file mode 100644 index 0000000..b4cc9da --- /dev/null +++ b/scripts/player/role.gd.uid @@ -0,0 +1 @@ +uid://dhomrampxola4 diff --git a/scripts/player/targeting.gd b/scripts/player/targeting.gd index 034aa69..d8db905 100644 --- a/scripts/player/targeting.gd +++ b/scripts/player/targeting.gd @@ -49,7 +49,7 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void: set_target(null) func _cycle_target() -> void: - var targets := get_tree().get_nodes_in_group("targetable") + var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") if targets.is_empty(): set_target(null) return @@ -70,23 +70,26 @@ func _on_enemy_engaged(_enemy: Node, target: Node) -> void: if not in_combat: in_combat = true if current_target == null: - _target_nearest() + _auto_target() -func _on_damage_dealt(_attacker: Node, target: Node, _amount: float) -> void: +func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void: if target == player: combat_timer = COMBAT_TIMEOUT if not in_combat: in_combat = true if current_target == null: - _target_nearest() + _auto_target() + elif attacker == player and in_combat: + combat_timer = COMBAT_TIMEOUT func _on_entity_died(entity: Node) -> void: if entity == current_target: set_target(null) if in_combat: - _target_nearest_except(entity) + _auto_target(entity) -func _target_nearest_except(exclude: Node = null) -> void: +func _auto_target(exclude: Node = null) -> void: + # Priorität 1: Nächster Gegner var enemies := get_tree().get_nodes_in_group("enemies") var nearest: Node3D = null var nearest_dist: float = INF @@ -98,16 +101,14 @@ func _target_nearest_except(exclude: Node = null) -> void: nearest = enemy if nearest: set_target(nearest) - -func _target_nearest() -> void: - var enemies := get_tree().get_nodes_in_group("enemies") - var nearest: Node3D = null - var nearest_dist: float = INF - for enemy in enemies: - if is_instance_valid(enemy): - var dist: float = player.global_position.distance_to(enemy.global_position) + return + # Priorität 2: Nächstes Portal + var portals := get_tree().get_nodes_in_group("portals") + for p in portals: + if is_instance_valid(p) and p != exclude: + var dist: float = player.global_position.distance_to(p.global_position) if dist < nearest_dist: nearest_dist = dist - nearest = enemy + nearest = p if nearest: set_target(nearest) diff --git a/scripts/portal/portal.gd b/scripts/portal/portal.gd index 8813e19..a41f7b9 100644 --- a/scripts/portal/portal.gd +++ b/scripts/portal/portal.gd @@ -1,4 +1,13 @@ extends StaticBody3D func _ready() -> void: - add_to_group("targetable") + add_to_group("portals") + EventBus.entity_died.connect(_on_entity_died) + +func _on_entity_died(entity: Node) -> void: + if entity == self: + queue_free() + +func _on_detection_area_body_entered(body: Node3D) -> void: + if body is CharacterBody3D and body.name == "Player": + EventBus.enemy_engaged.emit(self, body) diff --git a/scripts/portal/portal_spawner.gd b/scripts/portal/portal_spawner.gd deleted file mode 100644 index 3200847..0000000 --- a/scripts/portal/portal_spawner.gd +++ /dev/null @@ -1,30 +0,0 @@ -extends Node - -const SPAWN_COUNT := 3 -var thresholds := [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] -var triggered: Array[bool] = [false, false, false, false, false, false] -var enemy_scene: PackedScene = preload("res://scenes/enemy/enemy.tscn") - -@onready var portal: StaticBody3D = get_parent() -@onready var health: Node = get_parent().get_node("Health") - -func _process(_delta: float) -> void: - if 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_enemies() - -func _spawn_enemies() -> void: - var spawned: Array = [] - for j in range(SPAWN_COUNT): - var enemy: CharacterBody3D = enemy_scene.instantiate() - var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) - portal.get_parent().add_child(enemy) - enemy.global_position = portal.global_position + offset - enemy.spawn_position = portal.global_position - enemy.portal = portal - spawned.append(enemy) - EventBus.portal_spawn.emit(portal, spawned) diff --git a/scripts/portal/portal_spawner.gd.uid b/scripts/portal/portal_spawner.gd.uid deleted file mode 100644 index 635cafd..0000000 --- a/scripts/portal/portal_spawner.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://begrg74oh76pu diff --git a/scripts/resources/entity_stats.gd b/scripts/resources/entity_stats.gd new file mode 100644 index 0000000..acd4ac9 --- /dev/null +++ b/scripts/resources/entity_stats.gd @@ -0,0 +1,8 @@ +extends Resource +class_name EntityStats + +@export var max_health := 100.0 +@export var health_regen := 0.0 +@export var max_shield := 0.0 +@export var shield_regen_delay := 3.0 +@export var shield_regen_time := 5.0 diff --git a/scripts/resources/entity_stats.gd.uid b/scripts/resources/entity_stats.gd.uid new file mode 100644 index 0000000..d9c7bce --- /dev/null +++ b/scripts/resources/entity_stats.gd.uid @@ -0,0 +1 @@ +uid://ij663bdj2cgu