This commit is contained in:
Marek Lenczewski
2026-03-30 22:56:58 +02:00
parent d6715e9c3f
commit 04749104a0
49 changed files with 1406 additions and 171 deletions

View File

@@ -11,9 +11,12 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var uses_gcd: bool = true
@export var aoe_radius: float = 0.0
@export var icon: String = ""
@export var is_heal: bool = false
@export var passive_stat: String = "damage"
func execute(player: Node, targeting: Node) -> bool:
var dmg: float = _get_modified_damage(player, damage)
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)
@@ -25,28 +28,44 @@ func execute(player: Node, targeting: Node) -> bool:
return _execute_ult(player, targeting, dmg)
return false
func _get_modified_damage(player: Node, base: float) -> float:
func _get_modified_damage(player: Node, base: float, stat: String = "damage") -> float:
var combat: Node = player.get_node("Combat")
return combat.apply_passive(base)
return combat.apply_passive(base, stat)
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):
if is_heal:
return true
if not is_instance_valid(targeting.current_target):
return false
var dist: float = player.global_position.distance_to(target.global_position)
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
var target: Node3D = targeting.current_target
EventBus.damage_requested.emit(player, target, dmg)
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:
@@ -61,20 +80,38 @@ func _execute_aoe(player: Node, dmg: float) -> bool:
func _execute_utility(player: Node) -> bool:
var shield: Node = player.get_node_or_null("Shield")
if shield:
shield.current_shield = shield.max_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:
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)

View File

@@ -2,3 +2,6 @@ extends Resource
class_name AbilitySet
@export var abilities: Array[Ability] = []
@export var aa_damage: float = 10.0
@export var aa_range: float = 10.0
@export var aa_is_heal: bool = false

View File

@@ -10,6 +10,7 @@ func _ready() -> void:
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:
@@ -34,3 +35,11 @@ func take_damage(amount: float, attacker: Node) -> void:
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)

View File

@@ -7,11 +7,15 @@ 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:
@@ -25,6 +29,22 @@ func _process(delta: float) -> void:
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

View File

@@ -0,0 +1,16 @@
extends Node
func _ready() -> void:
var player: Node = get_tree().get_first_node_in_group("player")
if player:
GameState.restore_player(player)
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if entity.is_in_group("boss"):
await get_tree().create_timer(2.0).timeout
GameState.dungeon_cleared = true
GameState.returning_from_dungeon = false
GameState.clear_player()
EventBus.dungeon_cleared.emit()
get_tree().change_scene_to_file("res://scenes/world.tscn")

View File

@@ -0,0 +1 @@
uid://drn4h1lxx5t1j

5
scripts/enemy/boss.gd Normal file
View File

@@ -0,0 +1,5 @@
extends "res://scripts/enemy/enemy.gd"
func _ready() -> void:
super._ready()
add_to_group("boss")

View File

@@ -0,0 +1 @@
uid://bkehq3dqyp2yd

View File

@@ -10,6 +10,7 @@ var seconds_outside := 0.0
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
@@ -53,6 +54,12 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
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)

View File

@@ -15,3 +15,6 @@ signal respawn_tick(timer)
signal enemy_engaged(enemy, target)
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
signal portal_spawn(portal, enemies)
signal heal_requested(healer, target, amount)
signal portal_defeated(portal)
signal dungeon_cleared()

42
scripts/game_state.gd Normal file
View File

@@ -0,0 +1,42 @@
extends Node
var player_health: float = -1.0
var player_max_health: float = -1.0
var player_shield: float = -1.0
var player_max_shield: float = -1.0
var player_role: int = 1
var portal_position: Vector3 = Vector3.ZERO
var returning_from_dungeon := false
var dungeon_cleared := false
func save_player(player: Node) -> void:
var health: Node = player.get_node("Health")
var shield: Node = player.get_node("Shield")
var role: Node = player.get_node("Role")
player_health = health.current_health
player_max_health = health.max_health
player_shield = shield.current_shield
player_max_shield = shield.max_shield
player_role = role.current_role
func restore_player(player: Node) -> void:
if player_health < 0:
return
var health: Node = player.get_node("Health")
var shield: Node = player.get_node("Shield")
var role: Node = player.get_node("Role")
health.current_health = player_health
shield.current_shield = player_shield
role.set_role(player_role)
EventBus.health_changed.emit(player, health.current_health, health.max_health)
EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield)
func clear_player() -> void:
player_health = -1.0
player_shield = -1.0
func clear() -> void:
clear_player()
portal_position = Vector3.ZERO
returning_from_dungeon = false
dungeon_cleared = false

View File

@@ -0,0 +1 @@
uid://cp2vadwcd12sm

View File

@@ -1,9 +1,7 @@
extends Node
const GCD_TIME := 0.5
const AA_DAMAGE := 10.0
const AA_COOLDOWN := 1.0
const AA_RANGE := 20.0
const AA_COOLDOWN := 0.5
@onready var player: CharacterBody3D = get_parent()
@onready var targeting: Node = get_parent().get_node("Targeting")
@@ -36,12 +34,23 @@ func _auto_attack(delta: float) -> void:
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:
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
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])
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:
@@ -74,11 +83,11 @@ func _unhandled_input(event: InputEvent) -> void:
gcd_timer = GCD_TIME
return
func apply_passive(base_damage: float) -> float:
func apply_passive(base: float, stat: String = "damage") -> float:
for ability in abilities:
if ability and ability.type == Ability.Type.PASSIVE:
return base_damage * (1.0 + ability.damage / 100.0)
return base_damage
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()

View File

@@ -3,7 +3,9 @@ extends CanvasLayer
const GCD_TIME := 0.5
@onready var health_bar: ProgressBar = $HealthBar
@onready var health_label: Label = $HealthBar/HealthLabel
@onready var shield_bar: ProgressBar = $ShieldBar
@onready var shield_label: Label = $ShieldBar/ShieldLabel
@onready var respawn_label: Label = $RespawnTimer
@onready var class_icon: Label = $AbilityBar/ClassIcon/Label
@onready var ability_panels: Array = [
@@ -30,11 +32,13 @@ func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
health_bar.max_value = max_val
health_bar.value = current
health_label.text = "%d/%d" % [current, max_val]
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
shield_bar.max_value = max_val
shield_bar.value = current
shield_label.text = "%d/%d" % [current, max_val]
func _on_entity_died(entity: Node) -> void:
if entity.name == "Player":

View File

@@ -2,3 +2,7 @@ extends CharacterBody3D
func _ready() -> void:
add_to_group("player")
if GameState.returning_from_dungeon:
GameState.restore_player(self)
global_position = GameState.portal_position + Vector3(0, 1, -5)
GameState.returning_from_dungeon = false

View File

@@ -8,7 +8,7 @@ var spawn_position: Vector3
@onready var player: CharacterBody3D = get_parent()
func _ready() -> void:
spawn_position = player.global_position
spawn_position = Vector3(0, 1, -5)
EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void:

34
scripts/portal/gate.gd Normal file
View File

@@ -0,0 +1,34 @@
extends StaticBody3D
@export var target_scene: String = "res://scenes/dungeon/dungeon.tscn"
@export var is_exit: bool = false
var active := false
func _ready() -> void:
if not is_exit:
if GameState.dungeon_cleared:
queue_free()
return
get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
else:
get_tree().create_timer(1.0).timeout.connect(func() -> void: active = true)
func _check_overlapping() -> void:
active = true
for body in $GateArea.get_overlapping_bodies():
_on_gate_area_body_entered(body)
func _on_gate_area_body_entered(body: Node3D) -> void:
if not active:
return
if body is CharacterBody3D and body.name == "Player":
GameState.save_player(body)
if is_exit:
GameState.returning_from_dungeon = true
else:
GameState.portal_position = global_position
call_deferred("_change_scene")
func _change_scene() -> void:
get_tree().change_scene_to_file(target_scene)

View File

@@ -0,0 +1 @@
uid://ctci5mc3cd2ck

View File

@@ -1,12 +1,23 @@
extends StaticBody3D
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
func _ready() -> void:
add_to_group("portals")
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if entity == self:
queue_free()
if entity != self:
return
var gate: Node3D = GATE_SCENE.instantiate()
gate.global_position = global_position
get_parent().add_child(gate)
var enemies := get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if is_instance_valid(enemy):
enemy.queue_free()
EventBus.portal_defeated.emit(self)
queue_free()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":

View File

@@ -0,0 +1,45 @@
extends Node
const PORTAL_SCENE: PackedScene = preload("res://scenes/portal/portal.tscn")
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
const SPAWN_INTERVAL := 30.0
const MAX_PORTALS := 3
const MIN_DISTANCE := 20.0
const MAX_DISTANCE := 40.0
var portals: Array[Node] = []
var timer := 0.0
func _ready() -> void:
if GameState.portal_position != Vector3.ZERO and not GameState.dungeon_cleared:
call_deferred("_restore_gate")
else:
if GameState.dungeon_cleared:
GameState.clear()
call_deferred("_spawn_portal")
func _restore_gate() -> void:
var gate: Node3D = GATE_SCENE.instantiate()
get_parent().add_child(gate)
gate.global_position = GameState.portal_position
func _process(delta: float) -> void:
timer += delta
if timer >= SPAWN_INTERVAL:
timer = 0.0
_cleanup_dead()
if portals.size() < MAX_PORTALS:
_spawn_portal()
func _spawn_portal() -> void:
var angle: float = randf() * TAU
var distance: float = randf_range(MIN_DISTANCE, MAX_DISTANCE)
var pos := Vector3(cos(angle) * distance, 0, sin(angle) * distance)
var portal: Node3D = PORTAL_SCENE.instantiate()
get_parent().add_child(portal)
portal.global_position = pos
portals.append(portal)
print("Portal gespawnt bei: %s" % pos)
func _cleanup_dead() -> void:
portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))

View File

@@ -0,0 +1 @@
uid://cskx6o07iukwh