update
This commit is contained in:
@@ -1,124 +1,83 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.ability_use_requested.connect(_on_ability_use_requested)
|
||||
EventBus.ability_use.connect(_on_ability_use)
|
||||
|
||||
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:
|
||||
func _on_ability_use(_player: Node, ability_index: int) -> void:
|
||||
if not PlayerData.alive:
|
||||
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 player_base: BaseStats = Stats.get_base(player)
|
||||
var aa_cd: float = player_base.aa_cooldown if player_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()
|
||||
var ability_set: AbilitySet = PlayerData.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):
|
||||
if PlayerData.cooldowns[ability_index] > 0:
|
||||
return
|
||||
if ability.uses_gcd and not cooldown_system.is_gcd_ready(player):
|
||||
if ability.uses_gcd and PlayerData.gcd > 0:
|
||||
return
|
||||
var success: bool = _execute_ability(player, ability)
|
||||
var success: bool = _execute_ability(ability)
|
||||
if not success:
|
||||
return
|
||||
var player_base: BaseStats = Stats.get_base(player)
|
||||
var gcd_time: float = player_base.gcd_time if player_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)
|
||||
var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0
|
||||
PlayerData.cooldowns[ability_index] = ability.cooldown
|
||||
PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd)
|
||||
if gcd > 0:
|
||||
PlayerData.gcd = gcd
|
||||
|
||||
func _execute_ability(player: Node, ability: Ability) -> bool:
|
||||
var targeting: Node = player.get_node("Targeting")
|
||||
func _execute_ability(ability: Ability) -> bool:
|
||||
var stat: String = "heal" if ability.is_heal else "damage"
|
||||
var dmg: float = _apply_passive(player, ability.damage, stat)
|
||||
var dmg: float = _apply_passive(ability.damage, stat)
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
match ability.type:
|
||||
Ability.Type.SINGLE:
|
||||
return _execute_single(player, targeting, ability, dmg)
|
||||
return _execute_single(player, ability, dmg)
|
||||
Ability.Type.AOE:
|
||||
return _execute_aoe(player, ability, dmg)
|
||||
Ability.Type.UTILITY:
|
||||
return _execute_utility(player, ability)
|
||||
return _execute_utility(ability)
|
||||
Ability.Type.ULT:
|
||||
return _execute_ult(player, targeting, ability, dmg)
|
||||
return _execute_ult(player, 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 _apply_passive(base: float, stat: String) -> float:
|
||||
var mult: float = 1.0
|
||||
match stat:
|
||||
"damage": mult = PlayerData.buff_damage
|
||||
"heal": mult = PlayerData.buff_heal
|
||||
return base * mult
|
||||
|
||||
func _in_range(player: Node, targeting: Node, ability: Ability) -> bool:
|
||||
func _in_range(ability: Ability) -> bool:
|
||||
if ability.ability_range <= 0 or ability.is_heal:
|
||||
return true
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
var dist: float = player.global_position.distance_to(targeting.current_target.global_position)
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
var dist: float = player.global_position.distance_to(PlayerData.target.global_position)
|
||||
return dist <= ability.ability_range
|
||||
|
||||
func _execute_single(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool:
|
||||
func _execute_single(player: 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):
|
||||
if not _in_range(ability):
|
||||
return false
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
EventBus.damage_requested.emit(player, targeting.current_target, dmg)
|
||||
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
|
||||
if ability.element != 0:
|
||||
EventBus.element_damage_dealt.emit(player, targeting.current_target, dmg, ability.element)
|
||||
EventBus.element_damage_dealt.emit(player, PlayerData.target, dmg, ability.element)
|
||||
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:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
var dist: float = player.global_position.distance_to(enemy.global_position)
|
||||
if dist <= ability.ability_range:
|
||||
EventBus.damage_requested.emit(player, enemy, dmg)
|
||||
@@ -129,44 +88,35 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
|
||||
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:
|
||||
func _execute_utility(ability: Ability) -> bool:
|
||||
if PlayerData.max_shield <= 0:
|
||||
return false
|
||||
var shield: float = Stats.get_stat(player, "shield")
|
||||
var shield: float = PlayerData.shield
|
||||
if ability.damage > 0:
|
||||
shield = max_shield * (ability.damage / 100.0)
|
||||
shield = PlayerData.max_shield * (ability.damage / 100.0)
|
||||
else:
|
||||
if shield >= max_shield:
|
||||
if shield >= PlayerData.max_shield:
|
||||
return false
|
||||
shield = max_shield
|
||||
Stats.set_stat(player, "shield", shield)
|
||||
EventBus.shield_changed.emit(player, shield, max_shield)
|
||||
shield = PlayerData.max_shield
|
||||
PlayerData.set_shield(shield)
|
||||
return true
|
||||
|
||||
func _execute_ult(player: Node, targeting: Node, ability: Ability, dmg: float) -> bool:
|
||||
func _execute_ult(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")
|
||||
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):
|
||||
if not _in_range(ability):
|
||||
return false
|
||||
if not is_instance_valid(targeting.current_target):
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
var target: Node3D = targeting.current_target
|
||||
var target: Node3D = PlayerData.target
|
||||
EventBus.damage_requested.emit(player, target, dmg * 5.0)
|
||||
if ability.element != 0:
|
||||
EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element)
|
||||
var splash_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:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if enemy != target and is_instance_valid(enemy):
|
||||
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
|
||||
if enemy_dist <= splash_range:
|
||||
|
||||
@@ -23,8 +23,12 @@ func _update_combat_timers(delta: float) -> void:
|
||||
|
||||
func _decay_aggro(enemy: Node, delta: float) -> void:
|
||||
var table: Dictionary = tracker.aggro_tables[enemy]
|
||||
var base: BaseStats = Stats.get_base(enemy)
|
||||
var aggro_decay: float = base.aggro_decay if base is EnemyStats else 1.0
|
||||
var data_source: Node = tracker._get_data_source(enemy)
|
||||
var aggro_decay: float = 1.0
|
||||
if data_source:
|
||||
var base: EnemyStats = data_source.get_base(enemy)
|
||||
if base:
|
||||
aggro_decay = base.aggro_decay
|
||||
for player in table.keys():
|
||||
if is_in_combat(player):
|
||||
continue
|
||||
@@ -58,10 +62,11 @@ func spread_aggro(source: Node, attacker: Node, amount: float) -> void:
|
||||
func alert_nearby(enemy: Node, target: Node) -> void:
|
||||
var radius: float = tracker.get_alert_radius(enemy)
|
||||
for other in tracker.get_enemies_in_radius(enemy, radius):
|
||||
if "state" in other and other.state == other.State.IDLE:
|
||||
var data_source: Node = tracker._get_data_source(other)
|
||||
if data_source and data_source.get_stat(other, "state") == 0:
|
||||
tracker.add_aggro(other, target, 1.0)
|
||||
other.target = target
|
||||
other.state = other.State.CHASE
|
||||
data_source.set_stat(other, "target", target)
|
||||
data_source.set_stat(other, "state", 1)
|
||||
EventBus.enemy_engaged.emit(other, target)
|
||||
|
||||
func erase_entity(entity: Node) -> void:
|
||||
|
||||
@@ -14,14 +14,16 @@ func _ready() -> void:
|
||||
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:
|
||||
var data_source: Node = tracker._get_data_source(enemy)
|
||||
if data_source:
|
||||
var state: int = data_source.get_stat(enemy, "state")
|
||||
if state == 1 or state == 2:
|
||||
return
|
||||
tracker.add_player_in_range(enemy, player)
|
||||
tracker.add_aggro(enemy, player, 1.0)
|
||||
if "state" in enemy:
|
||||
enemy.target = player
|
||||
enemy.state = enemy.State.CHASE
|
||||
if data_source:
|
||||
data_source.set_stat(enemy, "target", player)
|
||||
data_source.set_stat(enemy, "state", 1)
|
||||
EventBus.enemy_engaged.emit(enemy, player)
|
||||
decay.alert_nearby(enemy, player)
|
||||
|
||||
@@ -33,8 +35,7 @@ func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
|
||||
return
|
||||
decay.reset_combat_timer(attacker)
|
||||
var multiplier := 1.0
|
||||
var role: Node = attacker.get_node_or_null("Role")
|
||||
if role and role.current_role == 0:
|
||||
if PlayerData.current_role == PlayerData.Role.TANK:
|
||||
multiplier = config.tank_multiplier
|
||||
var aggro: float = amount * multiplier
|
||||
tracker.add_aggro(target, attacker, aggro)
|
||||
|
||||
@@ -43,17 +43,19 @@ func get_top_target(table: Dictionary) -> Node:
|
||||
return top
|
||||
|
||||
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
|
||||
var data_source: Node = _get_data_source(enemy)
|
||||
if not data_source:
|
||||
return
|
||||
var state: int = data_source.get_stat(enemy, "state")
|
||||
if top and top != data_source.get_stat(enemy, "target"):
|
||||
data_source.set_stat(enemy, "target", top)
|
||||
if state == 0 or state == 3:
|
||||
data_source.set_stat(enemy, "state", 1)
|
||||
elif not top and state != 0 and state != 3:
|
||||
data_source.set_stat(enemy, "target", null)
|
||||
data_source.set_stat(enemy, "state", 3)
|
||||
|
||||
func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
||||
var result: Array = []
|
||||
@@ -65,8 +67,12 @@ func get_enemies_in_radius(source: Node, radius: float) -> Array:
|
||||
return result
|
||||
|
||||
func get_alert_radius(entity: Node) -> float:
|
||||
var base: BaseStats = Stats.get_base(entity)
|
||||
return base.alert_radius if base is EnemyStats else 10.0
|
||||
var data_source: Node = _get_data_source(entity)
|
||||
if data_source:
|
||||
var base: EnemyStats = data_source.get_base(entity)
|
||||
if base:
|
||||
return base.alert_radius
|
||||
return 10.0
|
||||
|
||||
func erase_entity(entity: Node) -> void:
|
||||
aggro_tables.erase(entity)
|
||||
@@ -74,9 +80,17 @@ func erase_entity(entity: Node) -> void:
|
||||
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
|
||||
var data_source: Node = _get_data_source(enemy)
|
||||
if data_source and data_source.get_stat(enemy, "target") == entity:
|
||||
data_source.set_stat(enemy, "target", null)
|
||||
data_source.set_stat(enemy, "state", 3)
|
||||
for enemy in players_in_range:
|
||||
if is_instance_valid(enemy):
|
||||
players_in_range[enemy].erase(entity)
|
||||
|
||||
func _get_data_source(entity: Node) -> Node:
|
||||
if entity.is_in_group("boss"):
|
||||
return BossData
|
||||
elif entity.is_in_group("enemies"):
|
||||
return EnemyData
|
||||
return null
|
||||
|
||||
91
systems/ai_system.gd
Normal file
91
systems/ai_system.gd
Normal file
@@ -0,0 +1,91 @@
|
||||
extends Node
|
||||
|
||||
enum State { IDLE, CHASE, ATTACK, RETURN }
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_process_group(delta, EnemyData)
|
||||
_process_group(delta, BossData)
|
||||
|
||||
func _process_group(delta: float, data_source: Node) -> void:
|
||||
for entity in data_source.entities:
|
||||
if not is_instance_valid(entity) or not data_source.is_alive(entity):
|
||||
continue
|
||||
var data: Dictionary = data_source.entities[entity]
|
||||
var state: int = data["state"]
|
||||
match state:
|
||||
State.IDLE:
|
||||
entity.velocity.x = 0
|
||||
entity.velocity.z = 0
|
||||
State.CHASE:
|
||||
_chase(entity, data, data_source)
|
||||
State.ATTACK:
|
||||
_attack(entity, data, data_source, delta)
|
||||
State.RETURN:
|
||||
_return_to_spawn(entity, data, data_source, delta)
|
||||
|
||||
func _chase(entity: Node, data: Dictionary, data_source: Node) -> void:
|
||||
if not is_instance_valid(data["target"]):
|
||||
data["state"] = State.RETURN
|
||||
return
|
||||
var base: EnemyStats = data_source.get_base(entity)
|
||||
var attack_range: float = base.attack_range
|
||||
var dist: float = entity.global_position.distance_to(data["target"].global_position)
|
||||
if dist <= attack_range:
|
||||
data["state"] = State.ATTACK
|
||||
return
|
||||
var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D")
|
||||
if not nav_agent:
|
||||
return
|
||||
nav_agent.target_position = data["target"].global_position
|
||||
var next_pos := nav_agent.get_next_path_position()
|
||||
var direction: Vector3 = (next_pos - entity.global_position).normalized()
|
||||
direction.y = 0
|
||||
entity.velocity.x = direction.x * base.speed
|
||||
entity.velocity.z = direction.z * base.speed
|
||||
|
||||
func _attack(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
|
||||
data["attack_timer"] -= delta
|
||||
if not is_instance_valid(data["target"]):
|
||||
data["state"] = State.RETURN
|
||||
return
|
||||
var base: EnemyStats = data_source.get_base(entity)
|
||||
var dist: float = entity.global_position.distance_to(data["target"].global_position)
|
||||
if dist > base.attack_range:
|
||||
data["state"] = State.CHASE
|
||||
return
|
||||
if data["attack_timer"] <= 0:
|
||||
data["attack_timer"] = base.attack_cooldown
|
||||
EventBus.damage_requested.emit(entity, data["target"], base.attack_damage)
|
||||
entity.velocity.x = 0
|
||||
entity.velocity.z = 0
|
||||
|
||||
func _return_to_spawn(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
|
||||
var spawn_pos: Vector3 = data["spawn_position"]
|
||||
var dist: float = entity.global_position.distance_to(spawn_pos)
|
||||
if dist < 1.0:
|
||||
data["state"] = State.IDLE
|
||||
entity.velocity.x = 0
|
||||
entity.velocity.z = 0
|
||||
return
|
||||
var base: EnemyStats = data_source.get_base(entity)
|
||||
var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D")
|
||||
if not nav_agent:
|
||||
return
|
||||
nav_agent.target_position = spawn_pos
|
||||
var next_pos := nav_agent.get_next_path_position()
|
||||
var direction: Vector3 = (next_pos - entity.global_position).normalized()
|
||||
direction.y = 0
|
||||
entity.velocity.x = direction.x * base.speed
|
||||
entity.velocity.z = direction.z * base.speed
|
||||
_regenerate(entity, data, data_source, delta)
|
||||
|
||||
func _regenerate(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void:
|
||||
var health: float = data["health"]
|
||||
var max_health: float = data["max_health"]
|
||||
if health < max_health:
|
||||
var base: EnemyStats = data_source.get_base(entity)
|
||||
var rate: float = base.regen_fast
|
||||
if health >= max_health * 0.99:
|
||||
rate = base.regen_slow
|
||||
health = min(health + max_health * rate * delta, max_health)
|
||||
data_source.set_health(entity, health)
|
||||
1
systems/ai_system.gd.uid
Normal file
1
systems/ai_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dokr1ut7ea541
|
||||
27
systems/attack_system.gd
Normal file
27
systems/attack_system.gd
Normal file
@@ -0,0 +1,27 @@
|
||||
extends Node
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not PlayerData.alive or not PlayerData.in_combat:
|
||||
return
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return
|
||||
if PlayerData.aa_timer > 0:
|
||||
return
|
||||
var ability_set: AbilitySet = PlayerData.ability_set
|
||||
if not ability_set:
|
||||
return
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if not player:
|
||||
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 = aa_damage * (PlayerData.buff_heal if aa_is_heal else PlayerData.buff_damage)
|
||||
if aa_is_heal:
|
||||
EventBus.heal_requested.emit(player, player, dmg)
|
||||
else:
|
||||
var dist: float = player.global_position.distance_to(PlayerData.target.global_position)
|
||||
if dist > aa_range:
|
||||
return
|
||||
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
|
||||
PlayerData.aa_timer = PlayerData.aa_cooldown
|
||||
1
systems/attack_system.gd.uid
Normal file
1
systems/attack_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvuds0uuffj6t
|
||||
62
systems/aura_system.gd
Normal file
62
systems/aura_system.gd
Normal file
@@ -0,0 +1,62 @@
|
||||
extends Node
|
||||
|
||||
const AURA_REFRESH := 0.5
|
||||
|
||||
var active_auras: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
for entity in active_auras.keys():
|
||||
if not is_instance_valid(entity):
|
||||
active_auras.erase(entity)
|
||||
continue
|
||||
for aura in active_auras[entity]:
|
||||
_propagate(entity, aura)
|
||||
|
||||
func _propagate(source_entity: Node, aura: Effect) -> void:
|
||||
if not source_entity is Node3D:
|
||||
return
|
||||
var buff_system: Node = get_node("../BuffSystem")
|
||||
var players := get_tree().get_nodes_in_group("player")
|
||||
for player in players:
|
||||
if not is_instance_valid(player) or not PlayerData.alive:
|
||||
continue
|
||||
var dist: float = source_entity.global_position.distance_to(player.global_position)
|
||||
if dist > aura.aura_radius:
|
||||
continue
|
||||
if buff_system.has_aura_buff(player, aura.effect_name, source_entity):
|
||||
buff_system.refresh_aura_buff(player, aura.effect_name, source_entity, AURA_REFRESH)
|
||||
else:
|
||||
var buff := Effect.new()
|
||||
buff.effect_name = aura.effect_name
|
||||
buff.type = Effect.Type.BUFF
|
||||
buff.stat = aura.stat
|
||||
buff.value = aura.value
|
||||
buff.duration = AURA_REFRESH
|
||||
buff.is_multiplier = aura.is_multiplier
|
||||
buff_system.apply_aura_buff(player, buff, source_entity)
|
||||
|
||||
func _on_role_changed(player: Node, _role_type: int) -> void:
|
||||
active_auras.erase(player)
|
||||
var ability_set: AbilitySet = PlayerData.ability_set
|
||||
if not ability_set:
|
||||
return
|
||||
for ability in ability_set.abilities:
|
||||
if ability and ability.type == Ability.Type.PASSIVE:
|
||||
var effect := Effect.new()
|
||||
effect.effect_name = ability.ability_name
|
||||
effect.type = Effect.Type.AURA
|
||||
effect.stat = ability.passive_stat
|
||||
effect.value = ability.damage / 100.0
|
||||
effect.duration = -1.0
|
||||
effect.is_multiplier = true
|
||||
effect.aura_radius = ability.ability_range
|
||||
if not active_auras.has(player):
|
||||
active_auras[player] = []
|
||||
active_auras[player].append(effect)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
active_auras.erase(entity)
|
||||
1
systems/aura_system.gd.uid
Normal file
1
systems/aura_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b17o3hfdm8uo6
|
||||
142
systems/buff_system.gd
Normal file
142
systems/buff_system.gd
Normal file
@@ -0,0 +1,142 @@
|
||||
extends Node
|
||||
|
||||
var active_buffs: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.effect_requested.connect(_on_effect_requested)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for entity in active_buffs.keys():
|
||||
if not is_instance_valid(entity):
|
||||
active_buffs.erase(entity)
|
||||
continue
|
||||
var entries: Array = active_buffs[entity]
|
||||
var i: int = entries.size() - 1
|
||||
while i >= 0:
|
||||
var entry: Dictionary = entries[i]
|
||||
var effect: Effect = entry["effect"]
|
||||
if effect.duration > 0:
|
||||
entry["remaining"] -= delta
|
||||
if entry["remaining"] <= 0:
|
||||
var is_aura_buff: bool = entry.get("is_aura_buff", false)
|
||||
entries.remove_at(i)
|
||||
if not is_aura_buff:
|
||||
EventBus.effect_expired.emit(entity, effect)
|
||||
_recalc(entity)
|
||||
i -= 1
|
||||
continue
|
||||
if effect.tick_interval > 0:
|
||||
entry["tick_timer"] -= delta
|
||||
if entry["tick_timer"] <= 0:
|
||||
entry["tick_timer"] += effect.tick_interval
|
||||
if not effect.is_multiplier and effect.type == Effect.Type.BUFF:
|
||||
var source: Node = entry["source"]
|
||||
if not is_instance_valid(source):
|
||||
source = entity
|
||||
EventBus.heal_requested.emit(source, entity, effect.value)
|
||||
i -= 1
|
||||
|
||||
func apply(target: Node, effect: Effect, source: Node) -> void:
|
||||
if effect.type != Effect.Type.BUFF and effect.type != Effect.Type.AURA:
|
||||
return
|
||||
if not active_buffs.has(target):
|
||||
active_buffs[target] = []
|
||||
var replaced := false
|
||||
var entries: Array = active_buffs[target]
|
||||
for i in range(entries.size()):
|
||||
if entries[i]["effect"].effect_name == effect.effect_name:
|
||||
entries[i]["effect"] = effect
|
||||
entries[i]["source"] = source
|
||||
entries[i]["remaining"] = effect.duration
|
||||
entries[i]["tick_timer"] = effect.tick_interval
|
||||
replaced = true
|
||||
break
|
||||
if not replaced:
|
||||
entries.append({
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval,
|
||||
})
|
||||
EventBus.effect_applied.emit(target, effect)
|
||||
if effect.is_multiplier:
|
||||
_recalc(target)
|
||||
|
||||
func apply_aura_buff(target: Node, effect: Effect, source: Node) -> void:
|
||||
if not active_buffs.has(target):
|
||||
active_buffs[target] = []
|
||||
var entry := {
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval,
|
||||
"aura_source": source,
|
||||
"is_aura_buff": true,
|
||||
}
|
||||
active_buffs[target].append(entry)
|
||||
if effect.is_multiplier:
|
||||
_recalc(target)
|
||||
|
||||
func has_aura_buff(target: Node, aura_name: String, source: Node) -> bool:
|
||||
if not active_buffs.has(target):
|
||||
return false
|
||||
for entry in active_buffs[target]:
|
||||
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
|
||||
return true
|
||||
return false
|
||||
|
||||
func refresh_aura_buff(target: Node, aura_name: String, source: Node, duration: float) -> void:
|
||||
if not active_buffs.has(target):
|
||||
return
|
||||
for entry in active_buffs[target]:
|
||||
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
|
||||
entry["remaining"] = duration
|
||||
return
|
||||
|
||||
func clear(entity: Node) -> void:
|
||||
active_buffs.erase(entity)
|
||||
_recalc(entity)
|
||||
|
||||
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
|
||||
if effect.type == Effect.Type.BUFF or effect.type == Effect.Type.AURA:
|
||||
apply(target, effect, source)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
active_buffs.erase(entity)
|
||||
_recalc(entity)
|
||||
|
||||
func _on_role_changed(player: Node, _role_type: int) -> void:
|
||||
_remove_permanent(player)
|
||||
|
||||
func _remove_permanent(entity: Node) -> void:
|
||||
if not active_buffs.has(entity):
|
||||
return
|
||||
var entries: Array = active_buffs[entity]
|
||||
var i: int = entries.size() - 1
|
||||
while i >= 0:
|
||||
if entries[i]["effect"].duration < 0:
|
||||
EventBus.effect_expired.emit(entity, entries[i]["effect"])
|
||||
entries.remove_at(i)
|
||||
i -= 1
|
||||
_recalc(entity)
|
||||
|
||||
func _recalc(entity: Node) -> void:
|
||||
var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 }
|
||||
if active_buffs.has(entity):
|
||||
for entry in active_buffs[entity]:
|
||||
var effect: Effect = entry["effect"]
|
||||
if effect.is_multiplier and effect.stat in mults:
|
||||
mults[effect.stat] += effect.value
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if entity == player:
|
||||
PlayerData.buff_damage = mults["damage"]
|
||||
PlayerData.buff_heal = mults["heal"]
|
||||
PlayerData.buff_shield = mults["shield"]
|
||||
if PlayerData.base:
|
||||
var new_max: float = PlayerData.base.max_shield * mults["shield"]
|
||||
PlayerData.max_shield = new_max
|
||||
PlayerData.shield = min(PlayerData.shield, new_max)
|
||||
PlayerData.set_shield(PlayerData.shield)
|
||||
EventBus.buff_changed.emit(entity, "damage", mults["damage"])
|
||||
1
systems/buff_system.gd.uid
Normal file
1
systems/buff_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://y2bm5ssu77wp
|
||||
@@ -1,73 +1,19 @@
|
||||
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"])
|
||||
if PlayerData.gcd > 0:
|
||||
PlayerData.gcd -= delta
|
||||
if PlayerData.aa_timer > 0:
|
||||
PlayerData.aa_timer -= delta
|
||||
for i in range(PlayerData.cooldowns.size()):
|
||||
if PlayerData.cooldowns[i] > 0:
|
||||
PlayerData.cooldowns[i] -= delta
|
||||
EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.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
|
||||
func _on_role_changed(_player: Node, _role_type: int) -> void:
|
||||
PlayerData.cooldowns.fill(0.0)
|
||||
PlayerData.max_cooldowns.fill(0.0)
|
||||
PlayerData.gcd = 0.0
|
||||
|
||||
@@ -1 +1,38 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.damage_requested.connect(_on_damage_requested)
|
||||
|
||||
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
|
||||
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:
|
||||
_apply_damage(target, remaining)
|
||||
|
||||
func _apply_damage(entity: Node, amount: float) -> void:
|
||||
if entity == _get_player():
|
||||
var health: float = PlayerData.health - amount
|
||||
if health < 0:
|
||||
health = 0
|
||||
PlayerData.set_health(health)
|
||||
elif entity.is_in_group("boss"):
|
||||
var health: float = BossData.get_stat(entity, "health") - amount
|
||||
if health < 0:
|
||||
health = 0
|
||||
BossData.set_health(entity, health)
|
||||
elif entity.is_in_group("enemies"):
|
||||
var health: float = EnemyData.get_stat(entity, "health") - amount
|
||||
if health < 0:
|
||||
health = 0
|
||||
EnemyData.set_health(entity, health)
|
||||
elif entity.is_in_group("portals"):
|
||||
var health: float = PortalData.get_stat(entity, "health") - amount
|
||||
if health < 0:
|
||||
health = 0
|
||||
PortalData.set_health(entity, health)
|
||||
|
||||
func _get_player() -> Node:
|
||||
return get_tree().get_first_node_in_group("player")
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://cbd1bryh0e2dw
|
||||
uid://cmy1kqo1pk1q8
|
||||
|
||||
65
systems/debuff_system.gd
Normal file
65
systems/debuff_system.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
extends Node
|
||||
|
||||
var active_debuffs: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.effect_requested.connect(_on_effect_requested)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for entity in active_debuffs.keys():
|
||||
if not is_instance_valid(entity):
|
||||
active_debuffs.erase(entity)
|
||||
continue
|
||||
var entries: Array = active_debuffs[entity]
|
||||
var i: int = entries.size() - 1
|
||||
while i >= 0:
|
||||
var entry: Dictionary = entries[i]
|
||||
var effect: Effect = entry["effect"]
|
||||
if effect.duration > 0:
|
||||
entry["remaining"] -= delta
|
||||
if entry["remaining"] <= 0:
|
||||
entries.remove_at(i)
|
||||
EventBus.effect_expired.emit(entity, effect)
|
||||
i -= 1
|
||||
continue
|
||||
if effect.tick_interval > 0:
|
||||
entry["tick_timer"] -= delta
|
||||
if entry["tick_timer"] <= 0:
|
||||
entry["tick_timer"] += effect.tick_interval
|
||||
var source: Node = entry["source"]
|
||||
if not is_instance_valid(source):
|
||||
source = entity
|
||||
EventBus.damage_requested.emit(source, entity, effect.value)
|
||||
i -= 1
|
||||
|
||||
func apply(target: Node, effect: Effect, source: Node) -> void:
|
||||
if effect.type != Effect.Type.DEBUFF:
|
||||
return
|
||||
if not active_debuffs.has(target):
|
||||
active_debuffs[target] = []
|
||||
var replaced := false
|
||||
var entries: Array = active_debuffs[target]
|
||||
for i in range(entries.size()):
|
||||
if entries[i]["effect"].effect_name == effect.effect_name:
|
||||
entries[i]["effect"] = effect
|
||||
entries[i]["source"] = source
|
||||
entries[i]["remaining"] = effect.duration
|
||||
entries[i]["tick_timer"] = effect.tick_interval
|
||||
replaced = true
|
||||
break
|
||||
if not replaced:
|
||||
entries.append({
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval,
|
||||
})
|
||||
EventBus.effect_applied.emit(target, effect)
|
||||
|
||||
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
|
||||
if effect.type == Effect.Type.DEBUFF:
|
||||
apply(target, effect, source)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
active_debuffs.erase(entity)
|
||||
1
systems/debuff_system.gd.uid
Normal file
1
systems/debuff_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ce12ledregjqx
|
||||
13
systems/dungeon_system.gd
Normal file
13
systems/dungeon_system.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
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
|
||||
PlayerData.dungeon_cleared = true
|
||||
PlayerData.returning_from_dungeon = false
|
||||
PlayerData.clear_cache()
|
||||
EventBus.dungeon_cleared.emit()
|
||||
get_tree().change_scene_to_file("res://scenes/world/world.tscn")
|
||||
1
systems/dungeon_system.gd.uid
Normal file
1
systems/dungeon_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://lc5n3uxi4fho
|
||||
@@ -1,190 +0,0 @@
|
||||
extends Node
|
||||
|
||||
var active_effects: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.effect_requested.connect(_on_effect_requested)
|
||||
|
||||
const AURA_REFRESH := 0.5
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for entity in active_effects.keys():
|
||||
if not is_instance_valid(entity):
|
||||
active_effects.erase(entity)
|
||||
continue
|
||||
var entries: Array = active_effects[entity]
|
||||
var i: int = entries.size() - 1
|
||||
while i >= 0:
|
||||
var entry: Dictionary = entries[i]
|
||||
var effect: Effect = entry["effect"]
|
||||
if effect.duration > 0:
|
||||
entry["remaining"] -= delta
|
||||
if entry["remaining"] <= 0:
|
||||
var is_aura_buff: bool = entry.get("is_aura_buff", false)
|
||||
entries.remove_at(i)
|
||||
if not is_aura_buff:
|
||||
EventBus.effect_expired.emit(entity, effect)
|
||||
_recalc_stat_buffs(entity)
|
||||
i -= 1
|
||||
continue
|
||||
if effect.tick_interval > 0:
|
||||
entry["tick_timer"] -= delta
|
||||
if entry["tick_timer"] <= 0:
|
||||
entry["tick_timer"] += effect.tick_interval
|
||||
_apply_tick(entity, entry)
|
||||
if effect.type == Effect.Type.AURA and effect.aura_radius > 0 and effect.duration < 0:
|
||||
_propagate_aura(entity, entry, effect)
|
||||
i -= 1
|
||||
|
||||
func _propagate_aura(source_entity: Node, _entry: Dictionary, aura: Effect) -> void:
|
||||
if not source_entity is Node3D:
|
||||
return
|
||||
var players := get_tree().get_nodes_in_group("player")
|
||||
for player in players:
|
||||
if not is_instance_valid(player) or not Stats.is_alive(player):
|
||||
continue
|
||||
var dist: float = source_entity.global_position.distance_to(player.global_position)
|
||||
if dist > aura.aura_radius:
|
||||
continue
|
||||
if _has_aura_buff(player, aura.effect_name, source_entity):
|
||||
_refresh_aura_buff(player, aura.effect_name, source_entity)
|
||||
else:
|
||||
var buff := Effect.new()
|
||||
buff.effect_name = aura.effect_name
|
||||
buff.type = Effect.Type.BUFF
|
||||
buff.stat = aura.stat
|
||||
buff.value = aura.value
|
||||
buff.duration = AURA_REFRESH
|
||||
buff.is_multiplier = aura.is_multiplier
|
||||
_apply_aura_buff(player, buff, source_entity)
|
||||
|
||||
func _has_aura_buff(target: Node, aura_name: String, source: Node) -> bool:
|
||||
if not active_effects.has(target):
|
||||
return false
|
||||
for entry in active_effects[target]:
|
||||
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _refresh_aura_buff(target: Node, aura_name: String, source: Node) -> void:
|
||||
if not active_effects.has(target):
|
||||
return
|
||||
for entry in active_effects[target]:
|
||||
if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source:
|
||||
entry["remaining"] = AURA_REFRESH
|
||||
return
|
||||
|
||||
func _apply_aura_buff(target: Node, effect: Effect, source: Node) -> void:
|
||||
if not active_effects.has(target):
|
||||
active_effects[target] = []
|
||||
var entry := {
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval,
|
||||
"aura_source": source,
|
||||
"is_aura_buff": true,
|
||||
}
|
||||
active_effects[target].append(entry)
|
||||
if effect.is_multiplier:
|
||||
_recalc_stat_buffs(target)
|
||||
|
||||
func apply_effect(target: Node, effect: Effect, source: Node) -> void:
|
||||
if not active_effects.has(target):
|
||||
active_effects[target] = []
|
||||
var replaced := false
|
||||
var entries: Array = active_effects[target]
|
||||
for i in range(entries.size()):
|
||||
if entries[i]["effect"].effect_name == effect.effect_name:
|
||||
entries[i]["effect"] = effect
|
||||
entries[i]["source"] = source
|
||||
entries[i]["remaining"] = effect.duration
|
||||
entries[i]["tick_timer"] = effect.tick_interval
|
||||
replaced = true
|
||||
break
|
||||
if not replaced:
|
||||
entries.append({
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval,
|
||||
})
|
||||
EventBus.effect_applied.emit(target, effect)
|
||||
if effect.is_multiplier:
|
||||
_recalc_stat_buffs(target)
|
||||
|
||||
func clear_effects(entity: Node) -> void:
|
||||
active_effects.erase(entity)
|
||||
if is_instance_valid(entity):
|
||||
_recalc_stat_buffs(entity)
|
||||
|
||||
func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void:
|
||||
apply_effect(target, effect, source)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
clear_effects(entity)
|
||||
|
||||
func _on_role_changed(player: Node, _role_type: int) -> void:
|
||||
_remove_permanent_effects(player)
|
||||
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
|
||||
for ability in ability_set.abilities:
|
||||
if ability and ability.type == Ability.Type.PASSIVE:
|
||||
var effect := Effect.new()
|
||||
effect.effect_name = ability.ability_name
|
||||
effect.type = Effect.Type.AURA
|
||||
effect.stat = ability.passive_stat
|
||||
effect.value = ability.damage / 100.0
|
||||
effect.duration = -1.0
|
||||
effect.is_multiplier = true
|
||||
effect.aura_radius = ability.ability_range
|
||||
apply_effect(player, effect, player)
|
||||
|
||||
func _remove_permanent_effects(entity: Node) -> void:
|
||||
if not active_effects.has(entity):
|
||||
return
|
||||
var entries: Array = active_effects[entity]
|
||||
var i: int = entries.size() - 1
|
||||
while i >= 0:
|
||||
if entries[i]["effect"].duration < 0:
|
||||
EventBus.effect_expired.emit(entity, entries[i]["effect"])
|
||||
entries.remove_at(i)
|
||||
i -= 1
|
||||
_recalc_stat_buffs(entity)
|
||||
|
||||
func _recalc_stat_buffs(entity: Node) -> void:
|
||||
var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 }
|
||||
if active_effects.has(entity):
|
||||
for entry in active_effects[entity]:
|
||||
var effect: Effect = entry["effect"]
|
||||
if effect.is_multiplier and effect.stat in mults:
|
||||
mults[effect.stat] += effect.value
|
||||
for stat in mults:
|
||||
Stats.set_stat(entity, "buff_" + stat, mults[stat])
|
||||
EventBus.buff_changed.emit(entity, stat, mults[stat])
|
||||
var base: BaseStats = Stats.get_base(entity)
|
||||
if base:
|
||||
var shield_mult: float = mults["shield"]
|
||||
var new_max: float = base.max_shield * shield_mult
|
||||
Stats.set_stat(entity, "max_shield", new_max)
|
||||
var shield: float = Stats.get_stat(entity, "shield")
|
||||
shield = min(shield, new_max)
|
||||
Stats.set_stat(entity, "shield", shield)
|
||||
EventBus.shield_changed.emit(entity, shield, new_max)
|
||||
|
||||
func _apply_tick(entity: Node, entry: Dictionary) -> void:
|
||||
var effect: Effect = entry["effect"]
|
||||
var source: Node = entry["source"]
|
||||
if not is_instance_valid(source):
|
||||
source = entity
|
||||
if not effect.is_multiplier:
|
||||
if effect.type == Effect.Type.DEBUFF:
|
||||
EventBus.damage_requested.emit(source, entity, effect.value)
|
||||
elif effect.type == Effect.Type.BUFF:
|
||||
EventBus.heal_requested.emit(source, entity, effect.value)
|
||||
@@ -1 +0,0 @@
|
||||
uid://drdlh6tq0dfwo
|
||||
@@ -1,36 +0,0 @@
|
||||
extends Node
|
||||
|
||||
var attack_timers: Dictionary = {}
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy) or not Stats.is_alive(enemy):
|
||||
continue
|
||||
if enemy.state != enemy.State.ATTACK:
|
||||
continue
|
||||
_handle_attack(enemy, delta)
|
||||
|
||||
func _handle_attack(enemy: Node, delta: float) -> void:
|
||||
if enemy not in attack_timers:
|
||||
attack_timers[enemy] = 0.0
|
||||
attack_timers[enemy] -= delta
|
||||
|
||||
if not is_instance_valid(enemy.target):
|
||||
enemy.state = enemy.State.RETURN
|
||||
return
|
||||
|
||||
var base: BaseStats = Stats.get_base(enemy)
|
||||
var attack_range: float = base.attack_range if base is EnemyStats else 2.0
|
||||
var dist: float = enemy.global_position.distance_to(enemy.target.global_position)
|
||||
if dist > attack_range:
|
||||
enemy.state = enemy.State.CHASE
|
||||
return
|
||||
|
||||
if attack_timers[enemy] <= 0:
|
||||
var attack_cooldown: float = base.attack_cooldown if base is EnemyStats else 1.5
|
||||
var attack_damage: float = base.attack_damage if base is EnemyStats else 5.0
|
||||
attack_timers[enemy] = attack_cooldown
|
||||
EventBus.damage_requested.emit(enemy, enemy.target, attack_damage)
|
||||
|
||||
enemy.velocity.x = 0
|
||||
enemy.velocity.z = 0
|
||||
@@ -1 +0,0 @@
|
||||
uid://bwhxu5586lc1l
|
||||
22
systems/heal_system.gd
Normal file
22
systems/heal_system.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.heal_requested.connect(_on_heal_requested)
|
||||
|
||||
func _on_heal_requested(_healer: Node, target: Node, amount: float) -> void:
|
||||
if target == _get_player():
|
||||
var health: float = min(PlayerData.health + amount, PlayerData.max_health)
|
||||
PlayerData.set_health(health)
|
||||
elif target.is_in_group("boss"):
|
||||
var health: float = BossData.get_stat(target, "health")
|
||||
var max_health: float = BossData.get_stat(target, "max_health")
|
||||
health = min(health + amount, max_health)
|
||||
BossData.set_health(target, health)
|
||||
elif target.is_in_group("enemies"):
|
||||
var health: float = EnemyData.get_stat(target, "health")
|
||||
var max_health: float = EnemyData.get_stat(target, "max_health")
|
||||
health = min(health + amount, max_health)
|
||||
EnemyData.set_health(target, health)
|
||||
|
||||
func _get_player() -> Node:
|
||||
return get_tree().get_first_node_in_group("player")
|
||||
1
systems/heal_system.gd.uid
Normal file
1
systems/heal_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8jyik37e4tjw
|
||||
@@ -1,14 +1,25 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.damage_requested.connect(_on_damage_requested)
|
||||
EventBus.heal_requested.connect(_on_heal_requested)
|
||||
_emit_initial.call_deferred()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for entity in Stats.entities:
|
||||
_regen_player(delta)
|
||||
_regen_entities(delta, EnemyData.entities)
|
||||
_regen_entities(delta, BossData.entities)
|
||||
|
||||
func _regen_player(delta: float) -> void:
|
||||
if not PlayerData.alive or PlayerData.health_regen <= 0:
|
||||
return
|
||||
if PlayerData.health < PlayerData.max_health:
|
||||
var health: float = min(PlayerData.health + PlayerData.health_regen * delta, PlayerData.max_health)
|
||||
PlayerData.set_health(health)
|
||||
|
||||
func _regen_entities(delta: float, entities: Dictionary) -> void:
|
||||
for entity in entities:
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var data: Dictionary = Stats.entities[entity]
|
||||
var data: Dictionary = entities[entity]
|
||||
if not data["alive"]:
|
||||
continue
|
||||
var regen: float = data["health_regen"]
|
||||
@@ -16,34 +27,6 @@ func _process(delta: float) -> void:
|
||||
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)
|
||||
func _emit_initial() -> void:
|
||||
EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health)
|
||||
EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://b3wkn5118dimy
|
||||
uid://h362ftxb0cns
|
||||
|
||||
168
systems/hud_system.gd
Normal file
168
systems/hud_system.gd
Normal file
@@ -0,0 +1,168 @@
|
||||
extends Node
|
||||
|
||||
const GCD_TIME := 0.5
|
||||
const ICON_SIZE := 20
|
||||
const FONT_SIZE := 14
|
||||
const BORDER_WIDTH := 2
|
||||
const MARGIN := 2
|
||||
|
||||
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
|
||||
var effect_container: HBoxContainer = null
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.shield_changed.connect(_on_shield_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.player_respawned.connect(_on_player_respawned)
|
||||
EventBus.respawn_tick.connect(_on_respawn_tick)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
EventBus.cooldown_tick.connect(_on_cooldown_tick)
|
||||
EventBus.effect_applied.connect(_on_effect_applied)
|
||||
EventBus.effect_expired.connect(_on_effect_expired)
|
||||
_init_hud.call_deferred()
|
||||
|
||||
func _init_hud() -> void:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
hud.get_node("RespawnTimer").visible = false
|
||||
effect_container = HBoxContainer.new()
|
||||
effect_container.name = "EffectContainer"
|
||||
effect_container.position = Vector2(10, 60)
|
||||
effect_container.add_theme_constant_override("separation", 3)
|
||||
hud.add_child(effect_container)
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
var bar: ProgressBar = hud.get_node("HealthBar")
|
||||
bar.max_value = max_val
|
||||
bar.value = current
|
||||
hud.get_node("HealthBar/HealthLabel").text = "%d/%d" % [current, max_val]
|
||||
|
||||
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
var bar: ProgressBar = hud.get_node("ShieldBar")
|
||||
bar.max_value = max_val
|
||||
bar.value = current
|
||||
hud.get_node("ShieldBar/ShieldLabel").text = "%d/%d" % [current, max_val]
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != PlayerData:
|
||||
return
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if hud:
|
||||
hud.get_node("RespawnTimer").visible = true
|
||||
|
||||
func _on_player_respawned(_player: Node) -> void:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if hud:
|
||||
hud.get_node("RespawnTimer").visible = false
|
||||
|
||||
func _on_respawn_tick(timer: float) -> void:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if hud:
|
||||
hud.get_node("RespawnTimer").text = str(ceil(timer))
|
||||
|
||||
func _on_role_changed(_player: Node, role_type: int) -> void:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label")
|
||||
match role_type:
|
||||
0: icon.text = "T"
|
||||
1: icon.text = "D"
|
||||
2: icon.text = "H"
|
||||
|
||||
func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void:
|
||||
var hud: CanvasLayer = _get_hud()
|
||||
if not hud:
|
||||
return
|
||||
var panels: Array = [
|
||||
hud.get_node("AbilityBar/Ability1"),
|
||||
hud.get_node("AbilityBar/Ability2"),
|
||||
hud.get_node("AbilityBar/Ability3"),
|
||||
hud.get_node("AbilityBar/Ability4"),
|
||||
hud.get_node("AbilityBar/Ability5"),
|
||||
]
|
||||
for i in range(min(panels.size(), cooldowns.size())):
|
||||
var panel: Panel = 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]
|
||||
|
||||
func _on_effect_applied(target: Node, effect: Effect) -> void:
|
||||
if target != PlayerData:
|
||||
return
|
||||
if effect_container:
|
||||
_add_icon(effect)
|
||||
|
||||
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
||||
if target != PlayerData:
|
||||
return
|
||||
if effect_container:
|
||||
_remove_icon(effect)
|
||||
|
||||
func _add_icon(effect: Effect) -> void:
|
||||
var panel := PanelContainer.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
match effect.type:
|
||||
Effect.Type.AURA:
|
||||
style.bg_color = Color(0.15, 0.15, 0.3, 1)
|
||||
style.border_color = Color(0.3, 0.5, 1.0, 1)
|
||||
Effect.Type.BUFF:
|
||||
style.bg_color = Color(0.15, 0.3, 0.15, 1)
|
||||
style.border_color = Color(0.3, 1.0, 0.3, 1)
|
||||
Effect.Type.DEBUFF:
|
||||
style.bg_color = Color(0.3, 0.15, 0.15, 1)
|
||||
style.border_color = Color(1.0, 0.3, 0.3, 1)
|
||||
style.set_border_width_all(BORDER_WIDTH)
|
||||
style.set_content_margin_all(MARGIN)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
var label := Label.new()
|
||||
label.text = effect.effect_name.left(1)
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
label.add_theme_color_override("font_color", Color.WHITE)
|
||||
label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
|
||||
panel.add_child(label)
|
||||
panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2)
|
||||
panel.set_meta("effect_type", effect.type)
|
||||
panel.set_meta("effect_name", effect.effect_name)
|
||||
var insert_idx := 0
|
||||
for child in effect_container.get_children():
|
||||
if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type:
|
||||
insert_idx += 1
|
||||
else:
|
||||
break
|
||||
effect_container.add_child(panel)
|
||||
effect_container.move_child(panel, insert_idx)
|
||||
|
||||
func _remove_icon(effect: Effect) -> void:
|
||||
for child in effect_container.get_children():
|
||||
if child.has_meta("effect_type") and child.has_meta("effect_name"):
|
||||
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
|
||||
child.queue_free()
|
||||
return
|
||||
|
||||
func _get_hud() -> CanvasLayer:
|
||||
return get_tree().get_first_node_in_group("hud") as CanvasLayer
|
||||
1
systems/hud_system.gd.uid
Normal file
1
systems/hud_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://da87wrxxuhws1
|
||||
225
systems/nameplate_system.gd
Normal file
225
systems/nameplate_system.gd
Normal file
@@ -0,0 +1,225 @@
|
||||
extends Node
|
||||
|
||||
const ICON_SIZE := 10
|
||||
const FONT_SIZE := 7
|
||||
const BORDER_WIDTH := 1
|
||||
const ICON_MARGIN := 0
|
||||
const BASE_HEIGHT := 29
|
||||
|
||||
var styles: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.shield_changed.connect(_on_shield_changed)
|
||||
EventBus.target_changed.connect(_on_target_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.effect_applied.connect(_on_effect_applied)
|
||||
EventBus.effect_expired.connect(_on_effect_expired)
|
||||
EventBus.portal_spawn.connect(_on_portal_spawn)
|
||||
_init_nameplates.call_deferred()
|
||||
|
||||
func _init_nameplates() -> void:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
_setup_nameplate(enemy)
|
||||
for portal in get_tree().get_nodes_in_group("portals"):
|
||||
_setup_nameplate(portal)
|
||||
|
||||
func _setup_nameplate(entity: Node) -> void:
|
||||
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
return
|
||||
var viewport: SubViewport = nameplate.get_node("SubViewport")
|
||||
nameplate.texture = viewport.get_texture()
|
||||
var border: ColorRect = viewport.get_node_or_null("Border")
|
||||
if border:
|
||||
border.visible = false
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy):
|
||||
continue
|
||||
var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
continue
|
||||
var health_bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar")
|
||||
var data_source: Node = _get_data_source(enemy)
|
||||
if not data_source:
|
||||
continue
|
||||
if enemy not in styles:
|
||||
var style_normal: StyleBoxFlat = health_bar.get_theme_stylebox("fill").duplicate()
|
||||
var style_aggro: StyleBoxFlat = style_normal.duplicate()
|
||||
style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1)
|
||||
styles[enemy] = { "normal": style_normal, "aggro": style_aggro }
|
||||
var s: Dictionary = styles[enemy]
|
||||
var enemy_target: Variant = data_source.get_stat(enemy, "target")
|
||||
if player and enemy_target == player:
|
||||
health_bar.add_theme_stylebox_override("fill", s["aggro"])
|
||||
else:
|
||||
health_bar.add_theme_stylebox_override("fill", s["normal"])
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
|
||||
if entity == PlayerData:
|
||||
return
|
||||
if not is_instance_valid(entity):
|
||||
return
|
||||
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
return
|
||||
if not nameplate.texture:
|
||||
_setup_nameplate(entity)
|
||||
var bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar")
|
||||
bar.max_value = max_val
|
||||
bar.value = current
|
||||
|
||||
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
|
||||
if entity == PlayerData:
|
||||
return
|
||||
if not is_instance_valid(entity):
|
||||
return
|
||||
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
return
|
||||
var bar: ProgressBar = nameplate.get_node_or_null("SubViewport/ShieldBar")
|
||||
if not bar:
|
||||
return
|
||||
if max_val <= 0:
|
||||
bar.visible = false
|
||||
return
|
||||
bar.visible = true
|
||||
bar.max_value = max_val
|
||||
bar.value = current
|
||||
|
||||
func _on_target_changed(_player: Node, target: Node) -> void:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy):
|
||||
continue
|
||||
var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar")
|
||||
if nameplate:
|
||||
nameplate.get_node("SubViewport/Border").visible = (target == enemy)
|
||||
for portal in get_tree().get_nodes_in_group("portals"):
|
||||
if not is_instance_valid(portal):
|
||||
continue
|
||||
var nameplate: Sprite3D = portal.get_node_or_null("Healthbar")
|
||||
if nameplate:
|
||||
nameplate.get_node("SubViewport/Border").visible = (target == portal)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != PlayerData and is_instance_valid(entity):
|
||||
styles.erase(entity)
|
||||
|
||||
func _on_effect_applied(target: Node, effect: Effect) -> void:
|
||||
if target == PlayerData:
|
||||
return
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
var nameplate: Sprite3D = target.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
return
|
||||
var container: HBoxContainer = _get_or_create_effect_container(nameplate)
|
||||
_add_icon(container, effect)
|
||||
_resize_viewport(nameplate)
|
||||
|
||||
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
||||
if target == PlayerData:
|
||||
return
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
var nameplate: Sprite3D = target.get_node_or_null("Healthbar")
|
||||
if not nameplate:
|
||||
return
|
||||
var container: HBoxContainer = nameplate.get_node_or_null("SubViewport/EffectContainer")
|
||||
if container:
|
||||
_remove_icon(container, effect)
|
||||
_resize_viewport.call_deferred(nameplate)
|
||||
|
||||
func _on_portal_spawn(_portal: Node, enemies: Array) -> void:
|
||||
for enemy in enemies:
|
||||
_setup_nameplate.call_deferred(enemy)
|
||||
|
||||
func _get_or_create_effect_container(nameplate: Sprite3D) -> HBoxContainer:
|
||||
var viewport: SubViewport = nameplate.get_node("SubViewport")
|
||||
var container: HBoxContainer = viewport.get_node_or_null("EffectContainer")
|
||||
if container:
|
||||
return container
|
||||
container = HBoxContainer.new()
|
||||
container.name = "EffectContainer"
|
||||
var health_bar: ProgressBar = viewport.get_node("HealthBar")
|
||||
var shield_bar: ProgressBar = viewport.get_node_or_null("ShieldBar")
|
||||
var y_pos: float = 0.0
|
||||
if shield_bar and shield_bar.visible:
|
||||
y_pos = shield_bar.offset_bottom + 2
|
||||
else:
|
||||
y_pos = health_bar.offset_bottom + 2
|
||||
container.position = Vector2(2, y_pos)
|
||||
container.add_theme_constant_override("separation", 1)
|
||||
viewport.add_child(container)
|
||||
return container
|
||||
|
||||
func _resize_viewport(nameplate: Sprite3D) -> void:
|
||||
var viewport: SubViewport = nameplate.get_node("SubViewport")
|
||||
var border: ColorRect = viewport.get_node("Border")
|
||||
var container: HBoxContainer = viewport.get_node_or_null("EffectContainer")
|
||||
if not container:
|
||||
return
|
||||
var icon_count := 0
|
||||
for child in container.get_children():
|
||||
if not child.is_queued_for_deletion():
|
||||
icon_count += 1
|
||||
if icon_count > 0:
|
||||
var needed: int = int(container.position.y) + ICON_SIZE + 4
|
||||
viewport.size.y = max(BASE_HEIGHT, needed)
|
||||
border.offset_bottom = viewport.size.y
|
||||
else:
|
||||
viewport.size.y = BASE_HEIGHT
|
||||
border.offset_bottom = BASE_HEIGHT
|
||||
|
||||
func _add_icon(container: HBoxContainer, effect: Effect) -> void:
|
||||
var panel := PanelContainer.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
match effect.type:
|
||||
Effect.Type.AURA:
|
||||
style.bg_color = Color(0.15, 0.15, 0.3, 1)
|
||||
style.border_color = Color(0.3, 0.5, 1.0, 1)
|
||||
Effect.Type.BUFF:
|
||||
style.bg_color = Color(0.15, 0.3, 0.15, 1)
|
||||
style.border_color = Color(0.3, 1.0, 0.3, 1)
|
||||
Effect.Type.DEBUFF:
|
||||
style.bg_color = Color(0.3, 0.15, 0.15, 1)
|
||||
style.border_color = Color(1.0, 0.3, 0.3, 1)
|
||||
style.set_border_width_all(BORDER_WIDTH)
|
||||
style.set_content_margin_all(ICON_MARGIN)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
var label := Label.new()
|
||||
label.text = effect.effect_name.left(1)
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
label.add_theme_color_override("font_color", Color.WHITE)
|
||||
label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE)
|
||||
panel.add_child(label)
|
||||
panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2)
|
||||
panel.set_meta("effect_type", effect.type)
|
||||
panel.set_meta("effect_name", effect.effect_name)
|
||||
var insert_idx := 0
|
||||
for child in container.get_children():
|
||||
if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type:
|
||||
insert_idx += 1
|
||||
else:
|
||||
break
|
||||
container.add_child(panel)
|
||||
container.move_child(panel, insert_idx)
|
||||
|
||||
func _remove_icon(container: HBoxContainer, effect: Effect) -> void:
|
||||
for child in container.get_children():
|
||||
if child.has_meta("effect_type") and child.has_meta("effect_name"):
|
||||
if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name:
|
||||
child.queue_free()
|
||||
return
|
||||
|
||||
func _get_data_source(entity: Node) -> Node:
|
||||
if entity.is_in_group("boss"):
|
||||
return BossData
|
||||
elif entity.is_in_group("enemies"):
|
||||
return EnemyData
|
||||
return null
|
||||
1
systems/nameplate_system.gd.uid
Normal file
1
systems/nameplate_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yijhaxo8anul
|
||||
21
systems/portal_system.gd
Normal file
21
systems/portal_system.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
extends Node
|
||||
|
||||
const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if not entity.is_in_group("portals"):
|
||||
return
|
||||
if not entity.is_inside_tree():
|
||||
return
|
||||
var pos: Vector3 = entity.global_position
|
||||
var gate: Node3D = GATE_SCENE.instantiate()
|
||||
entity.get_parent().add_child(gate)
|
||||
gate.global_position = pos
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(enemy):
|
||||
enemy.queue_free()
|
||||
EventBus.portal_defeated.emit(entity)
|
||||
entity.queue_free()
|
||||
1
systems/portal_system.gd.uid
Normal file
1
systems/portal_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5sqw08twxtnh
|
||||
@@ -1,48 +1,42 @@
|
||||
extends Node
|
||||
|
||||
var dead_players: Dictionary = {}
|
||||
var respawn_timer := 0.0
|
||||
var is_dead := false
|
||||
|
||||
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)
|
||||
if not is_dead:
|
||||
return
|
||||
respawn_timer -= delta
|
||||
EventBus.respawn_tick.emit(respawn_timer)
|
||||
if respawn_timer <= 0:
|
||||
_respawn()
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if not entity.is_in_group("player"):
|
||||
return
|
||||
if entity in dead_players:
|
||||
if is_dead:
|
||||
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
|
||||
is_dead = true
|
||||
respawn_timer = PlayerData.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("Ability").set_process_unhandled_input(false)
|
||||
entity.get_node("Targeting").set_process_unhandled_input(false)
|
||||
|
||||
func _respawn(player: Node) -> void:
|
||||
dead_players.erase(player)
|
||||
func _respawn() -> void:
|
||||
is_dead = false
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if not player:
|
||||
return
|
||||
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("Ability").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)
|
||||
PlayerData.respawn()
|
||||
|
||||
23
systems/role_system.gd
Normal file
23
systems/role_system.gd
Normal file
@@ -0,0 +1,23 @@
|
||||
extends Node
|
||||
|
||||
@export var tank_set: AbilitySet
|
||||
@export var damage_set: AbilitySet
|
||||
@export var healer_set: AbilitySet
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.role_change_requested.connect(_on_role_change_requested)
|
||||
_apply_role.call_deferred(PlayerData.current_role)
|
||||
|
||||
func _on_role_change_requested(_player: Node, role: int) -> void:
|
||||
_apply_role(role)
|
||||
|
||||
func _apply_role(role: int) -> void:
|
||||
PlayerData.current_role = role
|
||||
match role:
|
||||
PlayerData.Role.TANK:
|
||||
PlayerData.ability_set = tank_set
|
||||
PlayerData.Role.DAMAGE:
|
||||
PlayerData.ability_set = damage_set
|
||||
PlayerData.Role.HEALER:
|
||||
PlayerData.ability_set = healer_set
|
||||
PlayerData.set_role(role)
|
||||
1
systems/role_system.gd.uid
Normal file
1
systems/role_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cuwueo5v43kap
|
||||
@@ -1,10 +1,28 @@
|
||||
extends Node
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for entity in Stats.entities:
|
||||
_process_player(delta)
|
||||
_process_entities(delta, EnemyData.entities)
|
||||
_process_entities(delta, BossData.entities)
|
||||
|
||||
func _process_player(delta: float) -> void:
|
||||
if not PlayerData.alive or PlayerData.max_shield <= 0:
|
||||
return
|
||||
if PlayerData.shield < PlayerData.max_shield:
|
||||
PlayerData.shield_regen_timer += delta
|
||||
if PlayerData.shield_regen_timer >= PlayerData.shield_regen_delay:
|
||||
var regen_rate: float = PlayerData.max_shield / PlayerData.shield_regen_time
|
||||
var shield: float = PlayerData.shield + regen_rate * delta
|
||||
if shield >= PlayerData.max_shield:
|
||||
shield = PlayerData.max_shield
|
||||
EventBus.shield_regenerated.emit(get_tree().get_first_node_in_group("player"))
|
||||
PlayerData.set_shield(shield)
|
||||
|
||||
func _process_entities(delta: float, entities: Dictionary) -> void:
|
||||
for entity in entities:
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var data: Dictionary = Stats.entities[entity]
|
||||
var data: Dictionary = entities[entity]
|
||||
if not data["alive"]:
|
||||
continue
|
||||
var max_shield: float = data["max_shield"]
|
||||
@@ -22,17 +40,35 @@ func _process(delta: float) -> void:
|
||||
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")
|
||||
func absorb(target: Node, amount: float) -> float:
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if target == player:
|
||||
if PlayerData.shield <= 0:
|
||||
return amount
|
||||
PlayerData.shield_regen_timer = 0.0
|
||||
var absorbed: float = min(amount, PlayerData.shield)
|
||||
var shield: float = PlayerData.shield - absorbed
|
||||
PlayerData.set_shield(shield)
|
||||
if shield <= 0:
|
||||
EventBus.shield_broken.emit(target)
|
||||
return amount - absorbed
|
||||
var data_source: Node = _get_data_source(target)
|
||||
if not data_source:
|
||||
return amount
|
||||
var shield: float = data_source.get_stat(target, "shield")
|
||||
if shield == null or shield <= 0:
|
||||
return amount
|
||||
Stats.set_stat(entity, "shield_regen_timer", 0.0)
|
||||
data_source.set_stat(target, "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")
|
||||
data_source.set_shield(target, shield)
|
||||
if shield <= 0:
|
||||
EventBus.shield_broken.emit(entity)
|
||||
EventBus.shield_changed.emit(entity, shield, max_shield)
|
||||
EventBus.shield_broken.emit(target)
|
||||
return amount - absorbed
|
||||
|
||||
func _get_data_source(entity: Node) -> Node:
|
||||
if entity.is_in_group("boss"):
|
||||
return BossData
|
||||
elif entity.is_in_group("enemies"):
|
||||
return EnemyData
|
||||
return null
|
||||
|
||||
@@ -2,8 +2,6 @@ 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)
|
||||
@@ -11,22 +9,18 @@ func _ready() -> void:
|
||||
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 data: Dictionary = PortalData.entities.get(entity, {})
|
||||
if data.is_empty():
|
||||
return
|
||||
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
|
||||
var thresholds: Array = data["thresholds"]
|
||||
var triggered: Array = data["triggered"]
|
||||
var spawn_count: int = data["spawn_count"]
|
||||
for i in range(thresholds.size()):
|
||||
if not triggered[i] and ratio <= thresholds[i]:
|
||||
triggered[i] = true
|
||||
_spawn_enemies(entity, spawn_count)
|
||||
|
||||
func _spawn_enemies(portal: Node, count: int) -> void:
|
||||
@@ -36,8 +30,6 @@ func _spawn_enemies(portal: Node, count: int) -> void:
|
||||
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:
|
||||
@@ -49,4 +41,4 @@ func _spawn_enemies(portal: Node, count: int) -> void:
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity.is_in_group("portals"):
|
||||
portal_data.erase(entity)
|
||||
PortalData.deregister(entity)
|
||||
|
||||
66
systems/targeting_system.gd
Normal file
66
systems/targeting_system.gd
Normal file
@@ -0,0 +1,66 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.target_requested.connect(_on_target_requested)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.enemy_engaged.connect(_on_enemy_engaged)
|
||||
EventBus.damage_dealt.connect(_on_damage_dealt)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if PlayerData.in_combat:
|
||||
PlayerData.combat_timer -= delta
|
||||
if PlayerData.combat_timer <= 0:
|
||||
PlayerData.in_combat = false
|
||||
|
||||
func _on_target_requested(_player: Node, target: Node3D) -> void:
|
||||
PlayerData.set_target(target)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity == PlayerData.target:
|
||||
PlayerData.set_target(null)
|
||||
if PlayerData.in_combat:
|
||||
_auto_target(entity)
|
||||
|
||||
func _on_enemy_engaged(_enemy: Node, target: Node) -> void:
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if target == player:
|
||||
PlayerData.combat_timer = PlayerData.combat_timeout
|
||||
PlayerData.in_combat = true
|
||||
if PlayerData.target == null:
|
||||
_auto_target()
|
||||
|
||||
func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void:
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if target == player:
|
||||
PlayerData.combat_timer = PlayerData.combat_timeout
|
||||
if not PlayerData.in_combat:
|
||||
PlayerData.in_combat = true
|
||||
if PlayerData.target == null:
|
||||
_auto_target()
|
||||
elif attacker == player:
|
||||
PlayerData.in_combat = true
|
||||
PlayerData.combat_timer = PlayerData.combat_timeout
|
||||
|
||||
func _auto_target(exclude: Node = null) -> void:
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if not player:
|
||||
return
|
||||
var nearest: Node3D = null
|
||||
var nearest_dist: float = INF
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(enemy) and enemy != exclude:
|
||||
var dist: float = player.global_position.distance_to(enemy.global_position)
|
||||
if dist < nearest_dist:
|
||||
nearest_dist = dist
|
||||
nearest = enemy
|
||||
if nearest:
|
||||
PlayerData.set_target(nearest)
|
||||
return
|
||||
for p in get_tree().get_nodes_in_group("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 = p
|
||||
if nearest:
|
||||
PlayerData.set_target(nearest)
|
||||
1
systems/targeting_system.gd.uid
Normal file
1
systems/targeting_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvjmdmof4vcyr
|
||||
Reference in New Issue
Block a user