This commit is contained in:
Marek Lenczewski
2026-04-01 22:53:28 +02:00
parent b236cd52cb
commit e76c66eda6
70 changed files with 1016 additions and 732 deletions

View File

@@ -13,107 +13,3 @@ enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var icon: String = ""
@export var is_heal: bool = false
@export var passive_stat: String = "damage"
func execute(player: Node, targeting: Node) -> bool:
var stat: String = "heal" if is_heal else "damage"
var dmg: float = _get_modified_damage(player, damage, stat)
match type:
Type.SINGLE:
return _execute_single(player, targeting, dmg)
Type.AOE:
return _execute_aoe(player, dmg)
Type.UTILITY:
return _execute_utility(player)
Type.ULT:
return _execute_ult(player, targeting, dmg)
return false
func _get_modified_damage(player: Node, base: float, stat: String = "damage") -> float:
var combat: Node = player.get_node("Combat")
return combat.apply_passive(base, stat)
func _in_range(player: Node, targeting: Node) -> bool:
if ability_range <= 0:
return true
if is_heal:
return true
if not is_instance_valid(targeting.current_target):
return false
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
return dist <= ability_range
func _execute_single(player: Node, targeting: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
if not _in_range(player, targeting):
return false
if not is_instance_valid(targeting.current_target):
return false
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
func _execute_aoe(player: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
var players := player.get_tree().get_nodes_in_group("player")
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= ability_range:
EventBus.heal_requested.emit(player, p, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
var hit := false
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
var dist: float = player.global_position.distance_to(enemy.global_position)
if dist <= ability_range:
EventBus.damage_requested.emit(player, enemy, dmg)
hit = true
if hit:
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return hit
func _execute_utility(player: Node) -> bool:
var shield: Node = player.get_node_or_null("Shield")
if shield:
if damage > 0:
shield.current_shield = shield.max_shield * (damage / 100.0)
else:
if shield.current_shield >= shield.max_shield:
return false
shield.current_shield = shield.max_shield
EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield)
return true
return false
func _execute_ult(player: Node, targeting: Node, dmg: float) -> bool:
if is_heal:
EventBus.heal_requested.emit(player, player, dmg)
var players := player.get_tree().get_nodes_in_group("player")
var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range
for p in players:
if p != player and is_instance_valid(p):
var dist: float = player.global_position.distance_to(p.global_position)
if dist <= aoe_range:
EventBus.heal_requested.emit(player, p, dmg * 0.4)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
if not _in_range(player, targeting):
return false
if not is_instance_valid(targeting.current_target):
return false
var target: Node3D = targeting.current_target
EventBus.damage_requested.emit(player, target, dmg * 5.0)
var aoe_range: float = aoe_radius if aoe_radius > 0 else ability_range
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if enemy != target and is_instance_valid(enemy):
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
if enemy_dist <= aoe_range:
EventBus.damage_requested.emit(player, enemy, dmg * 2.0)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0)
return true

View File

@@ -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)

View File

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

View File

@@ -3,38 +3,55 @@ extends Sprite3D
@onready var viewport: SubViewport = $SubViewport
@onready var health_bar: ProgressBar = $SubViewport/HealthBar
@onready var border: ColorRect = $SubViewport/Border
@onready var health: Node = get_parent().get_node("Health")
@onready var parent_node: Node = get_parent()
var shield: Node = null
var shield_bar: ProgressBar = null
var style_normal: StyleBoxFlat
var style_aggro: StyleBoxFlat
func _ready() -> void:
texture = viewport.get_texture()
health_bar.max_value = health.max_health
shield = get_parent().get_node_or_null("Shield")
shield_bar = $SubViewport.get_node_or_null("ShieldBar")
if shield and shield_bar:
shield_bar.max_value = shield.max_shield
elif shield_bar:
shield_bar.visible = false
border.visible = false
style_normal = health_bar.get_theme_stylebox("fill").duplicate()
style_aggro = style_normal.duplicate()
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
EventBus.target_changed.connect(_on_target_changed)
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
_init_bars()
func _init_bars() -> void:
var max_health: Variant = Stats.get_stat(parent_node, "max_health")
if max_health != null:
health_bar.max_value = max_health
health_bar.value = Stats.get_stat(parent_node, "health")
var max_shield: Variant = Stats.get_stat(parent_node, "max_shield")
if shield_bar:
if max_shield != null and max_shield > 0:
shield_bar.max_value = max_shield
shield_bar.value = Stats.get_stat(parent_node, "shield")
else:
shield_bar.visible = false
func _process(_delta: float) -> void:
health_bar.value = health.current_health
if shield and shield_bar:
shield_bar.value = shield.current_shield
var player: Node = get_tree().get_first_node_in_group("player")
if player and "target" in parent_node and parent_node.target == player:
health_bar.add_theme_stylebox_override("fill", style_aggro)
else:
health_bar.add_theme_stylebox_override("fill", style_normal)
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity != parent_node:
return
health_bar.max_value = max_val
health_bar.value = current
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity != parent_node or shield_bar == null:
return
shield_bar.max_value = max_val
shield_bar.value = current
func _on_target_changed(_player: Node, target: Node) -> void:
border.visible = (target == get_parent())

View File

@@ -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

View File

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

View File

@@ -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)

View File

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

View File

@@ -11,6 +11,6 @@ func _on_entity_died(entity: Node) -> void:
await get_tree().create_timer(2.0).timeout
GameState.dungeon_cleared = true
GameState.returning_from_dungeon = false
GameState.clear_player()
GameState.clear()
EventBus.dungeon_cleared.emit()
get_tree().change_scene_to_file("res://scenes/world.tscn")

View File

@@ -2,56 +2,35 @@ extends CharacterBody3D
enum State { IDLE, CHASE, ATTACK, RETURN }
@export var stats: BaseStats
var state: int = State.IDLE
var target: Node3D = null
var spawn_position: Vector3
var portal: Node3D = null
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var health: Node = $Health
func _ready() -> void:
spawn_position = global_position
add_to_group("enemies")
Stats.register(self, stats)
EventBus.entity_died.connect(_on_entity_died)
func _exit_tree() -> void:
Stats.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity == self:
queue_free()
elif entity == target:
target = null
state = State.RETURN
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()
func _engage(new_target: Node3D) -> void:
if state == State.CHASE or state == State.ATTACK:
return
target = new_target
state = State.CHASE
var aggro: Node = get_node_or_null("EnemyAggro")
if aggro:
aggro.add_aggro(new_target, 1.0)
_alert_nearby()
func _alert_nearby() -> void:
var enemies := get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if enemy != self and is_instance_valid(enemy) and "state" in enemy:
if enemy.state == enemy.State.IDLE:
var dist: float = global_position.distance_to(enemy.global_position)
if dist <= 3.0:
enemy._engage(target)
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
_engage(body)
EventBus.enemy_engaged.emit(self, body)
EventBus.enemy_detected.emit(self, body)
func _on_detection_area_body_exited(body: Node3D) -> void:
if body == target and state == State.CHASE:
state = State.RETURN
target = null
func _on_detection_area_body_exited(_body: Node3D) -> void:
pass

View File

@@ -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

View File

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

View File

@@ -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

View File

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

View File

@@ -49,10 +49,14 @@ func _return_to_spawn(delta: float) -> void:
_regenerate(delta)
func _regenerate(delta: float) -> void:
var health: Node = enemy.get_node("Health")
if health.current_health < health.max_health:
var rate: float = REGEN_FAST if health.current_health < health.max_health else REGEN_SLOW
if health.current_health >= health.max_health * 0.99:
var health: float = Stats.get_stat(enemy, "health")
var max_health: float = Stats.get_stat(enemy, "max_health")
if health == null or max_health == null:
return
if health < max_health:
var rate: float = REGEN_FAST
if health >= max_health * 0.99:
rate = REGEN_SLOW
health.current_health = min(health.current_health + health.max_health * rate * delta, health.max_health)
EventBus.health_changed.emit(enemy, health.current_health, health.max_health)
health = min(health + max_health * rate * delta, max_health)
Stats.set_stat(enemy, "health", health)
EventBus.health_changed.emit(enemy, health, max_health)

View File

@@ -1,20 +1,46 @@
extends Node
# Intentionen (Input → System)
signal ability_use_requested(player, ability_index)
signal auto_attack_tick(attacker)
signal target_requested(player, target)
signal enemy_detected(enemy, player)
# Ergebnisse (System → Node)
signal combat_state_changed(player, in_combat)
signal enemy_state_changed(enemy, new_state)
signal enemy_target_changed(enemy, target)
# Kampf
signal attack_executed(attacker, position, direction, damage)
signal damage_dealt(attacker, target, damage)
signal damage_requested(attacker, target, amount)
signal heal_requested(healer, target, amount)
# Entity
signal entity_died(entity)
signal health_changed(entity, current, max_val)
signal shield_changed(entity, current, max_val)
signal shield_broken(entity)
signal shield_regenerated(entity)
signal regeneration_changed(entity, current, max_val)
# Spieler
signal target_changed(player, target)
signal player_respawned(player)
signal role_changed(player, role_type)
signal damage_requested(attacker, target, amount)
signal health_changed(entity, current, max_val)
signal shield_changed(entity, current, max_val)
signal respawn_tick(timer)
signal enemy_engaged(enemy, target)
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
# Buff
signal buff_changed(entity, stat, value)
# Gegner
signal enemy_engaged(enemy, target)
# Portal
signal portal_spawn(portal, enemies)
signal heal_requested(healer, target, amount)
signal portal_defeated(portal)
# Dungeon
signal dungeon_cleared()

View File

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

View File

@@ -1,93 +1,9 @@
extends Node
const GCD_TIME := 0.5
const AA_COOLDOWN := 0.5
@onready var player: CharacterBody3D = get_parent()
@onready var targeting: Node = get_parent().get_node("Targeting")
@onready var role: Node = get_parent().get_node("Role")
var abilities: Array = []
var cooldowns: Array[float] = [0.0, 0.0, 0.0, 0.0, 0.0]
var max_cooldowns: Array[float] = [0.0, 0.0, 0.0, 0.0, 0.0]
var gcd_timer := 0.0
var aa_timer := 0.0
func _ready() -> void:
_load_abilities()
EventBus.role_changed.connect(_on_role_changed)
func _process(delta: float) -> void:
if gcd_timer > 0:
gcd_timer -= delta
for i in range(cooldowns.size()):
if cooldowns[i] > 0:
cooldowns[i] -= delta
EventBus.cooldown_tick.emit(cooldowns, max_cooldowns, gcd_timer)
_auto_attack(delta)
func _auto_attack(delta: float) -> void:
aa_timer -= delta
if aa_timer > 0:
return
if not targeting.in_combat or not targeting.current_target:
return
if not is_instance_valid(targeting.current_target):
return
var ability_set: AbilitySet = role.get_ability_set()
if not ability_set:
return
var aa_damage: float = ability_set.aa_damage
var aa_range: float = ability_set.aa_range
var aa_is_heal: bool = ability_set.aa_is_heal
var dmg: float = apply_passive(aa_damage, "heal" if aa_is_heal else "damage")
if aa_is_heal:
EventBus.heal_requested.emit(player, player, dmg)
print("AA Heal: %s an %s" % [dmg, player.name])
else:
var dist := player.global_position.distance_to(targeting.current_target.global_position)
if dist > aa_range:
return
var target_name: String = targeting.current_target.name
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
print("AA: %s Schaden an %s" % [dmg, target_name])
aa_timer = AA_COOLDOWN
func _load_abilities() -> void:
var ability_set: AbilitySet = role.get_ability_set()
if ability_set:
abilities = ability_set.abilities
else:
abilities = []
cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0]
max_cooldowns = [0.0, 0.0, 0.0, 0.0, 0.0]
gcd_timer = 0.0
func _unhandled_input(event: InputEvent) -> void:
for i in range(min(abilities.size(), 5)):
if event.is_action_pressed("ability_%s" % (i + 1)) and abilities[i]:
if abilities[i].type == Ability.Type.PASSIVE:
return
if cooldowns[i] > 0:
return
if abilities[i].uses_gcd and gcd_timer > 0:
return
var success: bool = abilities[i].execute(player, targeting)
if not success:
return
var ability_cd: float = abilities[i].cooldown
var gcd_cd: float = GCD_TIME if abilities[i].uses_gcd else 0.0
cooldowns[i] = ability_cd
max_cooldowns[i] = max(ability_cd, gcd_cd)
if abilities[i].uses_gcd:
gcd_timer = GCD_TIME
for i in range(5):
if event.is_action_pressed("ability_%s" % (i + 1)):
EventBus.ability_use_requested.emit(player, i)
return
func apply_passive(base: float, stat: String = "damage") -> float:
for ability in abilities:
if ability and ability.type == Ability.Type.PASSIVE and ability.passive_stat == stat:
return base * (1.0 + ability.damage / 100.0)
return base
func _on_role_changed(_player: Node, _role_type: int) -> void:
_load_abilities()

View File

@@ -1,8 +1,5 @@
extends Node
const SPEED := 5.0
const JUMP_VELOCITY := 4.5
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var player: CharacterBody3D = get_parent()
@@ -11,8 +8,12 @@ func _physics_process(delta: float) -> void:
if not player.is_on_floor():
player.velocity.y -= gravity * delta
var base: BaseStats = Stats.get_base(player)
var speed: float = base.speed if base is PlayerStats else 5.0
var jump_velocity: float = base.jump_velocity if base is PlayerStats else 4.5
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
player.velocity.y = JUMP_VELOCITY
player.velocity.y = jump_velocity
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var camera_pivot := player.get_node("CameraPivot") as Node3D
@@ -26,10 +27,10 @@ func _physics_process(delta: float) -> void:
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
if direction:
player.velocity.x = direction.x * SPEED
player.velocity.z = direction.z * SPEED
player.velocity.x = direction.x * speed
player.velocity.z = direction.z * speed
else:
player.velocity.x = move_toward(player.velocity.x, 0, SPEED)
player.velocity.z = move_toward(player.velocity.z, 0, SPEED)
player.velocity.x = move_toward(player.velocity.x, 0, speed)
player.velocity.z = move_toward(player.velocity.z, 0, speed)
player.move_and_slide()

View File

@@ -1,8 +1,28 @@
extends CharacterBody3D
@export var stats: BaseStats
func _ready() -> void:
add_to_group("player")
Stats.register(self, stats)
var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system")
if cooldown_system:
cooldown_system.register(self, 5)
if GameState.returning_from_dungeon:
GameState.restore_player(self)
global_position = GameState.portal_position + Vector3(0, 1, -5)
GameState.returning_from_dungeon = false
elif GameState.dungeon_cleared:
GameState.clear()
var health: float = Stats.get_stat(self, "health")
var max_health: float = Stats.get_stat(self, "max_health")
var shield: float = Stats.get_stat(self, "shield")
var max_shield: float = Stats.get_stat(self, "max_shield")
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
func _exit_tree() -> void:
Stats.deregister(self)
var cooldown_system: Node = get_tree().get_first_node_in_group("cooldown_system")
if cooldown_system:
cooldown_system.deregister(self)

View File

@@ -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)

View File

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

View File

@@ -1,11 +1,17 @@
extends StaticBody3D
@export var stats: BaseStats
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
func _ready() -> void:
add_to_group("portals")
Stats.register(self, stats)
EventBus.entity_died.connect(_on_entity_died)
func _exit_tree() -> void:
Stats.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return

View File

@@ -1,5 +1,5 @@
extends Resource
class_name EntityStats
class_name BaseStats
@export var max_health := 100.0
@export var health_regen := 0.0

View File

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

View File

@@ -0,0 +1,2 @@
extends EnemyStats
class_name BossStats

View File

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

View 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

View File

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

View File

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

View 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

View File

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

View 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]

View File

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

53
scripts/stats.gd Normal file
View 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
View File

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

View 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

View File

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

View 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

View File

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

View 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)

View File

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

View 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

View File

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

View File

@@ -0,0 +1 @@
extends Node

View File

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

View 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

View File

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

View 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)

View File

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

View 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)

View File

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

View 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

View File

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

View 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)

View File

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

View File

@@ -39,7 +39,6 @@ func _spawn_portal() -> void:
get_parent().add_child(portal)
portal.global_position = pos
portals.append(portal)
print("Portal gespawnt bei: %s" % pos)
func _cleanup_dead() -> void:
portals = portals.filter(func(p: Node) -> bool: return is_instance_valid(p))