diff --git a/plan.md b/plan.md index 5eb4821..1e64f7a 100644 --- a/plan.md +++ b/plan.md @@ -64,15 +64,15 @@ - 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, Fähigkeiten 1-5 - - 1 Single: Anvisiertes Ziel angreifen (10 Schaden, Distanzprüfung) - - 2 AOE: Alle Gegner im Bereich (5 Schaden, halber Single-Schaden) - - 3 Utility: Schild-Regeneration sofort zurücksetzen - - 4 Ult: 4x Schaden an anvisiertem Ziel + 2x AOE-Schaden um das Ziel herum - - 5 Passive: 50% mehr Schaden (permanent aktiv) +- 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, Kampfmodus bei Gegner-Angriff, Auto-Targeting auf nächsten Gegner - event_bus.gd — Autoload-Singleton, globale Signals -- enemy.gd — Gegner-Kern, State Machine (Idle, Verfolgen, Angreifen, Zurückkehren) +- enemy.gd — Gegner-Kern, State Machine (Idle, Verfolgen, Angreifen, Zurückkehren), Aggro bei Schaden, alarmiert Gegner in 3m - enemy_movement.gd — Navigation zum Ziel/Spawnpunkt - enemy_combat.gd — Angriff über Event (damage_requested) - health.gd — Leben, 1/s Regeneration, Tod bei 0 (wiederverwendbar) @@ -83,7 +83,7 @@ - enemy_healthbar.gd — Liest Health/Shield vom Gegner, aktualisiert Balken über dem Gegner, gelber Rand bei Anvisierung ## Abilities (Resources) -- ability.gd (Resource) — name, type, damage, range, execute() +- 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 @@ -105,3 +105,4 @@ - shield_changed(entity, current, max) — Schild hat sich verändert - respawn_tick(timer) — Respawn-Countdown Update - enemy_engaged(enemy, target) — Gegner hat Spieler anvisiert +- cooldown_tick(cooldowns, max_cooldowns, gcd_timer) — Cooldown-Update für HUD diff --git a/resources/abilities/aoe_attack.tres b/resources/abilities/aoe_attack.tres index 06a8022..e14c163 100644 --- a/resources/abilities/aoe_attack.tres +++ b/resources/abilities/aoe_attack.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="Ability" load_steps=2 format=3] +[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://bpx3l13iuynfv"] -[ext_resource type="Script" path="res://scripts/abilities/ability.gd" id="1"] +[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"] [resource] script = ExtResource("1") @@ -8,4 +8,5 @@ ability_name = "AOE" type = 1 damage = 5.0 ability_range = 5.0 +cooldown = 3.0 icon = "2" diff --git a/resources/abilities/passive_damage_boost.tres b/resources/abilities/passive_damage_boost.tres index b391ddc..0c34466 100644 --- a/resources/abilities/passive_damage_boost.tres +++ b/resources/abilities/passive_damage_boost.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="Ability" load_steps=2 format=3] +[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dadpl32yujwhe"] -[ext_resource type="Script" path="res://scripts/abilities/ability.gd" id="1"] +[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"] [resource] script = ExtResource("1") @@ -8,4 +8,5 @@ ability_name = "Damage Boost" type = 4 damage = 50.0 ability_range = 0.0 -icon = "5" +uses_gcd = false +icon = "P" diff --git a/resources/abilities/single_attack.tres b/resources/abilities/single_attack.tres index 788f352..547d673 100644 --- a/resources/abilities/single_attack.tres +++ b/resources/abilities/single_attack.tres @@ -1,11 +1,10 @@ -[gd_resource type="Resource" script_class="Ability" load_steps=2 format=3] +[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dwvc8b3cmce8l"] -[ext_resource type="Script" path="res://scripts/abilities/ability.gd" id="1"] +[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"] [resource] script = ExtResource("1") ability_name = "Single" -type = 0 damage = 10.0 -ability_range = 3.0 +ability_range = 20.0 icon = "1" diff --git a/resources/abilities/ult_burst.tres b/resources/abilities/ult_burst.tres index 089db12..9a16570 100644 --- a/resources/abilities/ult_burst.tres +++ b/resources/abilities/ult_burst.tres @@ -1,11 +1,12 @@ -[gd_resource type="Resource" script_class="Ability" load_steps=2 format=3] +[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://s32wvlww2ls2"] -[ext_resource type="Script" path="res://scripts/abilities/ability.gd" id="1"] +[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"] [resource] script = ExtResource("1") ability_name = "Burst" type = 3 damage = 10.0 -ability_range = 5.0 +ability_range = 20.0 +cooldown = 30.0 icon = "4" diff --git a/resources/abilities/utility_shield_reset.tres b/resources/abilities/utility_shield_reset.tres index 9ed38c7..6f8fe83 100644 --- a/resources/abilities/utility_shield_reset.tres +++ b/resources/abilities/utility_shield_reset.tres @@ -1,11 +1,12 @@ -[gd_resource type="Resource" script_class="Ability" load_steps=2 format=3] +[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://du0hyuuj26ea0"] -[ext_resource type="Script" path="res://scripts/abilities/ability.gd" id="1"] +[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scripts/abilities/ability.gd" id="1"] [resource] script = ExtResource("1") ability_name = "Shield Reset" type = 2 -damage = 0.0 ability_range = 0.0 +cooldown = 5.0 +uses_gcd = false icon = "3" diff --git a/scenes/hud/hud.tscn b/scenes/hud/hud.tscn index ee3777d..c385404 100644 --- a/scenes/hud/hud.tscn +++ b/scenes/hud/hud.tscn @@ -109,6 +109,13 @@ vertical_alignment = 1 custom_minimum_size = Vector2(45, 45) theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability1"] +layout_mode = 1 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +visible = false + [node name="Label" type="Label" parent="AbilityBar/Ability1"] layout_mode = 1 anchors_preset = 15 @@ -124,6 +131,13 @@ vertical_alignment = 1 custom_minimum_size = Vector2(45, 45) theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability2"] +layout_mode = 1 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +visible = false + [node name="Label" type="Label" parent="AbilityBar/Ability2"] layout_mode = 1 anchors_preset = 15 @@ -139,6 +153,13 @@ vertical_alignment = 1 custom_minimum_size = Vector2(45, 45) theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability3"] +layout_mode = 1 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +visible = false + [node name="Label" type="Label" parent="AbilityBar/Ability3"] layout_mode = 1 anchors_preset = 15 @@ -154,6 +175,13 @@ vertical_alignment = 1 custom_minimum_size = Vector2(45, 45) theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability4"] +layout_mode = 1 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +visible = false + [node name="Label" type="Label" parent="AbilityBar/Ability4"] layout_mode = 1 anchors_preset = 15 @@ -169,6 +197,13 @@ vertical_alignment = 1 custom_minimum_size = Vector2(45, 45) theme_override_styles/panel = SubResource("StyleBoxFlat_ability_round") +[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability5"] +layout_mode = 1 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +visible = false + [node name="Label" type="Label" parent="AbilityBar/Ability5"] layout_mode = 1 anchors_preset = 15 diff --git a/scripts/abilities/ability.gd b/scripts/abilities/ability.gd index eeadaf1..f20e0e9 100644 --- a/scripts/abilities/ability.gd +++ b/scripts/abilities/ability.gd @@ -7,55 +7,68 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } @export var type: Type = Type.SINGLE @export var damage: float = 0.0 @export var ability_range: float = 3.0 +@export var cooldown: float = 0.0 +@export var uses_gcd: bool = true @export var icon: String = "" -func execute(player: Node, targeting: Node) -> void: +func execute(player: Node, targeting: Node) -> bool: var dmg: float = _get_modified_damage(player, damage) match type: Type.SINGLE: - _execute_single(player, targeting, dmg) + return _execute_single(player, targeting, dmg) Type.AOE: - _execute_aoe(player, dmg) + return _execute_aoe(player, dmg) Type.UTILITY: - _execute_utility(player) + return _execute_utility(player) Type.ULT: - _execute_ult(player, targeting, dmg) + return _execute_ult(player, targeting, dmg) + return false func _get_modified_damage(player: Node, base: float) -> float: var combat: Node = player.get_node("Combat") return combat.apply_passive(base) -func _execute_single(player: Node, targeting: Node, dmg: float) -> void: +func _in_range(player: Node, targeting: Node) -> bool: + if ability_range <= 0: + return true var target: Node3D = targeting.current_target if not target or not is_instance_valid(target): - return + return false var dist: float = player.global_position.distance_to(target.global_position) - if dist > ability_range: - return + return dist <= ability_range + +func _execute_single(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) EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) + return true -func _execute_aoe(player: Node, dmg: float) -> void: +func _execute_aoe(player: Node, dmg: float) -> bool: + 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) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, 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) -> void: +func _execute_utility(player: Node) -> bool: var shield: Node = player.get_node_or_null("Shield") if shield: 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) -> void: +func _execute_ult(player: Node, targeting: Node, dmg: float) -> bool: + if not _in_range(player, targeting): + return false var target: Node3D = targeting.current_target - if not target or not is_instance_valid(target): - return - var dist: float = player.global_position.distance_to(target.global_position) - if dist > ability_range: - return EventBus.damage_requested.emit(player, target, dmg * 4.0) var enemies := player.get_tree().get_nodes_in_group("enemies") for enemy in enemies: @@ -64,3 +77,4 @@ func _execute_ult(player: Node, targeting: Node, dmg: float) -> void: if enemy_dist <= ability_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) + return true diff --git a/scripts/enemy/enemy.gd b/scripts/enemy/enemy.gd index 4aaa4ba..7bd45c0 100644 --- a/scripts/enemy/enemy.gd +++ b/scripts/enemy/enemy.gd @@ -13,6 +13,7 @@ func _ready() -> void: spawn_position = global_position add_to_group("enemies") EventBus.entity_died.connect(_on_entity_died) + EventBus.damage_dealt.connect(_on_damage_dealt) func _on_entity_died(entity: Node) -> void: if entity == self: @@ -26,10 +27,29 @@ func _physics_process(delta: float) -> void: velocity.y -= gravity * delta move_and_slide() +func _on_damage_dealt(attacker: Node, damage_target: Node, _amount: float) -> void: + if damage_target == self and attacker.name == "Player": + _engage(attacker) + +func _engage(new_target: Node3D) -> void: + if state == State.CHASE or state == State.ATTACK: + return + target = new_target + state = State.CHASE + _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): + if enemy.state == enemy.State.IDLE: + var dist: float = global_position.distance_to(enemy.global_position) + if dist <= 3.0: + enemy._engage(target) + func _on_detection_area_body_entered(body: Node3D) -> void: if body is CharacterBody3D and body.name == "Player": - target = body - state = State.CHASE + _engage(body) EventBus.enemy_engaged.emit(self, body) func _on_detection_area_body_exited(body: Node3D) -> void: diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index b0085d1..a2ed436 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -13,3 +13,4 @@ signal health_changed(entity, current, max_val) signal shield_changed(entity, current, max_val) signal respawn_tick(timer) signal enemy_engaged(enemy, target) +signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer) diff --git a/scripts/player/combat.gd b/scripts/player/combat.gd index aced7b4..ef43bbe 100644 --- a/scripts/player/combat.gd +++ b/scripts/player/combat.gd @@ -1,28 +1,56 @@ extends Node +const GCD_TIME := 1.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") 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 func _ready() -> void: _load_abilities() EventBus.class_changed.connect(_on_class_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) + func _load_abilities() -> void: var ability_set: AbilitySet = player_class.get_ability_set() if ability_set: abilities = ability_set.abilities else: abilities = [] + cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0] + max_cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0] + gcd_timer = 0.0 func _unhandled_input(event: InputEvent) -> void: for i in range(min(abilities.size(), 5)): if event.is_action_pressed("ability_%s" % (i + 1)) and abilities[i]: if abilities[i].type == Ability.Type.PASSIVE: return - abilities[i].execute(player, targeting) + 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_damage: float) -> float: diff --git a/scripts/player/hud.gd b/scripts/player/hud.gd index e77d149..cdba447 100644 --- a/scripts/player/hud.gd +++ b/scripts/player/hud.gd @@ -1,11 +1,20 @@ extends CanvasLayer +const GCD_TIME := 1.0 + @onready var health_bar: ProgressBar = $HealthBar @onready var shield_bar: ProgressBar = $ShieldBar @onready var respawn_label: Label = $RespawnTimer @onready var class_icon: Label = $AbilityBar/ClassIcon/Label +@onready var ability_panels: Array = [ + $AbilityBar/Ability1, + $AbilityBar/Ability2, + $AbilityBar/Ability3, + $AbilityBar/Ability4, + $AbilityBar/Ability5, +] -var player_node: Node = null +var ability_labels: Array[String] = ["1", "2", "3", "4", "P"] func _ready() -> void: respawn_label.visible = false @@ -15,6 +24,7 @@ func _ready() -> void: EventBus.player_respawned.connect(_on_player_respawned) EventBus.class_changed.connect(_on_class_changed) EventBus.respawn_tick.connect(_on_respawn_tick) + EventBus.cooldown_tick.connect(_on_cooldown_tick) func _on_health_changed(entity: Node, current: float, max_val: float) -> void: if entity.name == "Player": @@ -41,3 +51,22 @@ func _on_class_changed(_player: Node, class_type: int) -> void: 0: class_icon.text = "T" 1: class_icon.text = "D" 2: class_icon.text = "H" + +func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void: + for i in range(min(ability_panels.size(), cooldowns.size())): + var panel: Panel = ability_panels[i] + var label: Label = panel.get_node("Label") + var overlay: ColorRect = panel.get_node("CooldownOverlay") + var cd: float = cooldowns[i] + var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0 + var active_cd: float = max(cd, gcd) + var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME + + if active_cd > 0: + var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0) + overlay.visible = true + overlay.anchor_bottom = ratio + label.text = str(ceil(active_cd)) + else: + overlay.visible = false + label.text = ability_labels[i]