refactor
This commit is contained in:
@@ -1,129 +0,0 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.ability_use.connect(_on_ability_use)
|
||||
|
||||
func _on_ability_use(_player: Node, ability_index: int) -> void:
|
||||
if not PlayerData.alive:
|
||||
return
|
||||
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
|
||||
if PlayerData.cooldowns[ability_index] > 0:
|
||||
return
|
||||
if ability.uses_gcd and PlayerData.gcd > 0:
|
||||
return
|
||||
var success: bool = _execute_ability(ability)
|
||||
if not success:
|
||||
return
|
||||
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(ability: Ability) -> bool:
|
||||
var stat: String = "heal" if ability.is_heal else "damage"
|
||||
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, ability, dmg)
|
||||
Ability.Type.AOE:
|
||||
return _execute_aoe(player, ability, dmg)
|
||||
Ability.Type.UTILITY:
|
||||
return _execute_utility(ability)
|
||||
Ability.Type.ULT:
|
||||
return _execute_ult(player, ability, dmg)
|
||||
return false
|
||||
|
||||
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 * PlayerData.level_scale
|
||||
|
||||
func _in_range(ability: Ability) -> bool:
|
||||
if ability.ability_range <= 0 or ability.is_heal:
|
||||
return true
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
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, 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(ability):
|
||||
return false
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
EventBus.damage_requested.emit(player, PlayerData.target, dmg)
|
||||
if ability.element != 0:
|
||||
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)
|
||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||
return true
|
||||
var hit := false
|
||||
var targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
|
||||
for target in targets:
|
||||
var dist: float = player.global_position.distance_to(target.global_position)
|
||||
if dist <= ability.ability_range:
|
||||
EventBus.damage_requested.emit(player, target, dmg)
|
||||
if ability.element != 0:
|
||||
EventBus.element_damage_dealt.emit(player, target, dmg, ability.element)
|
||||
hit = true
|
||||
if hit:
|
||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||
return hit
|
||||
|
||||
func _execute_utility(ability: Ability) -> bool:
|
||||
if PlayerData.max_shield <= 0:
|
||||
return false
|
||||
var shield: float = PlayerData.shield
|
||||
if ability.damage > 0:
|
||||
shield = PlayerData.max_shield * (ability.damage / 100.0)
|
||||
else:
|
||||
if shield >= PlayerData.max_shield:
|
||||
return false
|
||||
shield = PlayerData.max_shield
|
||||
PlayerData.set_shield(shield)
|
||||
return true
|
||||
|
||||
func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool:
|
||||
if ability.is_heal:
|
||||
EventBus.heal_requested.emit(player, player, dmg)
|
||||
var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
|
||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
|
||||
return true
|
||||
if not _in_range(ability):
|
||||
return false
|
||||
if not is_instance_valid(PlayerData.target):
|
||||
return false
|
||||
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 splash_targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
|
||||
for other in splash_targets:
|
||||
if other != target and is_instance_valid(other):
|
||||
var other_dist: float = target.global_position.distance_to(other.global_position)
|
||||
if other_dist <= splash_range:
|
||||
EventBus.damage_requested.emit(player, other, dmg * 2.0)
|
||||
if ability.element != 0:
|
||||
EventBus.element_damage_dealt.emit(player, other, dmg * 2.0, ability.element)
|
||||
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0)
|
||||
return true
|
||||
@@ -1 +0,0 @@
|
||||
uid://h0hts425epc6
|
||||
@@ -1,8 +0,0 @@
|
||||
extends Resource
|
||||
class_name AggroConfig
|
||||
|
||||
@export var combat_timeout := 5.0
|
||||
@export var tank_multiplier := 2.0
|
||||
@export var heal_multiplier := 0.5
|
||||
@export var spread_multiplier := 0.5
|
||||
@export var exponential_decay_factor := 0.01
|
||||
@@ -1 +0,0 @@
|
||||
uid://b3gwl1wweld2x
|
||||
@@ -1,6 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="AggroConfig" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/aggro/aggro_config.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
@@ -1,73 +0,0 @@
|
||||
extends Node
|
||||
|
||||
var tracker: Node
|
||||
var config: AggroConfig
|
||||
var last_damage_time: Dictionary = {}
|
||||
|
||||
func process(delta: float) -> void:
|
||||
_update_combat_timers(delta)
|
||||
for enemy in tracker.aggro_tables.keys():
|
||||
if not is_instance_valid(enemy):
|
||||
tracker.aggro_tables.erase(enemy)
|
||||
tracker.players_in_range.erase(enemy)
|
||||
continue
|
||||
_decay_aggro(enemy, delta)
|
||||
tracker.update_target(enemy)
|
||||
|
||||
func _update_combat_timers(delta: float) -> void:
|
||||
for player in last_damage_time.keys():
|
||||
if not is_instance_valid(player):
|
||||
last_damage_time.erase(player)
|
||||
else:
|
||||
last_damage_time[player] += delta
|
||||
|
||||
func _decay_aggro(enemy: Node, delta: float) -> void:
|
||||
var table: Dictionary = tracker.aggro_tables[enemy]
|
||||
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
|
||||
var time_since_combat: float = last_damage_time.get(player, config.combat_timeout) - config.combat_timeout
|
||||
var decay: float = aggro_decay * delta
|
||||
decay += _exponential_decay(table[player], time_since_combat, delta)
|
||||
table[player] -= decay
|
||||
if table[player] <= 0:
|
||||
table.erase(player)
|
||||
|
||||
func reset_combat_timer(player: Node) -> void:
|
||||
last_damage_time[player] = 0.0
|
||||
|
||||
func is_in_combat(player: Node) -> bool:
|
||||
if tracker.is_player_in_any_range(player):
|
||||
return true
|
||||
return last_damage_time.get(player, config.combat_timeout + 1.0) < config.combat_timeout
|
||||
|
||||
func _exponential_decay(aggro: float, time_outside: float, delta: float) -> float:
|
||||
if time_outside <= 0:
|
||||
return 0.0
|
||||
return aggro * config.exponential_decay_factor * pow(2, time_outside) * delta
|
||||
|
||||
func spread_aggro(source: Node, attacker: Node, amount: float) -> void:
|
||||
if not is_instance_valid(source):
|
||||
return
|
||||
var radius: float = tracker.get_alert_radius(source)
|
||||
for enemy in tracker.get_enemies_in_radius(source, radius):
|
||||
tracker.add_aggro(enemy, attacker, amount)
|
||||
|
||||
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):
|
||||
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)
|
||||
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:
|
||||
last_damage_time.erase(entity)
|
||||
@@ -1 +0,0 @@
|
||||
uid://cysg30lud2ta2
|
||||
@@ -1,53 +0,0 @@
|
||||
extends Node
|
||||
|
||||
var tracker: Node
|
||||
var decay: Node
|
||||
var config: AggroConfig
|
||||
|
||||
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)
|
||||
EventBus.enemy_lost.connect(_on_enemy_lost)
|
||||
|
||||
func _on_enemy_detected(enemy: Node, player: Node) -> void:
|
||||
if not enemy.is_in_group("enemies"):
|
||||
return
|
||||
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 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)
|
||||
|
||||
func _on_enemy_lost(enemy: Node, player: Node) -> void:
|
||||
tracker.remove_player_in_range(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
|
||||
decay.reset_combat_timer(attacker)
|
||||
var multiplier := 1.0
|
||||
if PlayerData.current_role == PlayerData.Role.TANK:
|
||||
multiplier = config.tank_multiplier
|
||||
var aggro: float = amount * multiplier
|
||||
tracker.add_aggro(target, attacker, aggro)
|
||||
decay.spread_aggro(target, attacker, aggro * config.spread_multiplier)
|
||||
|
||||
func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void:
|
||||
if not healer.is_in_group("player"):
|
||||
return
|
||||
for enemy in tracker.aggro_tables:
|
||||
if is_instance_valid(enemy) and healer in tracker.aggro_tables[enemy]:
|
||||
tracker.add_aggro(enemy, healer, amount * config.heal_multiplier)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
tracker.erase_entity(entity)
|
||||
decay.erase_entity(entity)
|
||||
@@ -1 +0,0 @@
|
||||
uid://cyffo1g4uhmwh
|
||||
@@ -1,17 +0,0 @@
|
||||
extends Node
|
||||
|
||||
@export var config: AggroConfig = preload("res://systems/aggro/aggro_config.tres")
|
||||
|
||||
@onready var tracker: Node = $AggroTracker
|
||||
@onready var decay: Node = $AggroDecay
|
||||
@onready var events: Node = $AggroEvents
|
||||
|
||||
func _ready() -> void:
|
||||
decay.tracker = tracker
|
||||
decay.config = config
|
||||
events.tracker = tracker
|
||||
events.decay = decay
|
||||
events.config = config
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
decay.process(delta)
|
||||
@@ -1 +0,0 @@
|
||||
uid://cm7ehl2pexcst
|
||||
@@ -1,96 +0,0 @@
|
||||
extends Node
|
||||
|
||||
var aggro_tables: Dictionary = {}
|
||||
var players_in_range: Dictionary = {}
|
||||
|
||||
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 remove_aggro(enemy: Node, player: Node, amount: float) -> void:
|
||||
if enemy in aggro_tables and player in aggro_tables[enemy]:
|
||||
aggro_tables[enemy][player] -= amount
|
||||
if aggro_tables[enemy][player] <= 0:
|
||||
aggro_tables[enemy].erase(player)
|
||||
|
||||
func add_player_in_range(enemy: Node, player: Node) -> void:
|
||||
if enemy not in players_in_range:
|
||||
players_in_range[enemy] = []
|
||||
if player not in players_in_range[enemy]:
|
||||
players_in_range[enemy].append(player)
|
||||
|
||||
func remove_player_in_range(enemy: Node, player: Node) -> void:
|
||||
if enemy in players_in_range:
|
||||
players_in_range[enemy].erase(player)
|
||||
|
||||
func is_player_in_any_range(player: Node) -> bool:
|
||||
for enemy in players_in_range:
|
||||
if is_instance_valid(enemy) and player in players_in_range[enemy]:
|
||||
return true
|
||||
return false
|
||||
|
||||
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 update_target(enemy: Node) -> void:
|
||||
var table: Dictionary = aggro_tables[enemy]
|
||||
var top: Node = get_top_target(table)
|
||||
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 = []
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if enemy != source and is_instance_valid(enemy):
|
||||
var dist: float = source.global_position.distance_to(enemy.global_position)
|
||||
if dist <= radius:
|
||||
result.append(enemy)
|
||||
return result
|
||||
|
||||
func get_alert_radius(entity: Node) -> float:
|
||||
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)
|
||||
players_in_range.erase(entity)
|
||||
for enemy in aggro_tables:
|
||||
if is_instance_valid(enemy):
|
||||
aggro_tables[enemy].erase(entity)
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
uid://c7gsu2qddsor6
|
||||
110
systems/aggro_system.gd
Normal file
110
systems/aggro_system.gd
Normal file
@@ -0,0 +1,110 @@
|
||||
extends Node
|
||||
|
||||
const COMBAT_TIMEOUT: float = 5.0
|
||||
const TANK_MULT: float = 2.0
|
||||
const HEAL_MULT: float = 0.5
|
||||
const SPREAD: float = 0.5
|
||||
const DECAY_PER_SEC: float = 1.0
|
||||
|
||||
var aggro: Dictionary = {}
|
||||
var combat_timers: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.damage_dealt.connect(_on_damage_dealt)
|
||||
EventBus.heal_requested.connect(_on_heal_requested)
|
||||
EventBus.entity_died.connect(_on_died)
|
||||
EventBus.entity_deregistered.connect(_on_dereg)
|
||||
EventBus.enemy_detected.connect(_on_enemy_detected)
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
var _accum: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
_accum += delta
|
||||
if _accum < 0.20:
|
||||
return
|
||||
var dt: float = _accum
|
||||
_accum = 0.0
|
||||
for enemy in aggro.keys():
|
||||
if not is_instance_valid(enemy):
|
||||
continue
|
||||
var t: float = combat_timers.get(enemy, 0.0)
|
||||
if t > 0.0:
|
||||
combat_timers[enemy] = max(0.0, t - dt)
|
||||
else:
|
||||
var table: Dictionary = aggro[enemy]
|
||||
var to_remove: Array = []
|
||||
for player in table.keys():
|
||||
table[player] = max(0.0, table[player] - DECAY_PER_SEC * dt)
|
||||
if table[player] <= 0.0:
|
||||
to_remove.append(player)
|
||||
for p in to_remove:
|
||||
table.erase(p)
|
||||
if not aggro[enemy].is_empty():
|
||||
EventBus.enemy_engaged.emit(enemy, _top_target(enemy))
|
||||
|
||||
func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void:
|
||||
if not is_instance_valid(target) or not is_instance_valid(attacker):
|
||||
return
|
||||
if target.is_in_group("enemies"):
|
||||
var role: int = int(Stats.get_stat(attacker, "role", GameState.ROLE_DAMAGE))
|
||||
var mult: float = TANK_MULT if role == GameState.ROLE_TANK else 1.0
|
||||
_add(target, attacker, amount * mult)
|
||||
_spread(target, attacker, amount * SPREAD)
|
||||
combat_timers[target] = COMBAT_TIMEOUT
|
||||
|
||||
func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void:
|
||||
if not is_instance_valid(healer):
|
||||
return
|
||||
for enemy in aggro.keys():
|
||||
if not is_instance_valid(enemy):
|
||||
continue
|
||||
if healer in aggro[enemy]:
|
||||
_add(enemy, healer, amount * HEAL_MULT)
|
||||
|
||||
func _on_enemy_detected(enemy: Node, player: Node) -> void:
|
||||
_add(enemy, player, 1.0)
|
||||
combat_timers[enemy] = COMBAT_TIMEOUT
|
||||
|
||||
func _on_died(entity: Node) -> void:
|
||||
aggro.erase(entity)
|
||||
combat_timers.erase(entity)
|
||||
for enemy in aggro.keys():
|
||||
if entity in aggro[enemy]:
|
||||
aggro[enemy].erase(entity)
|
||||
|
||||
func _on_dereg(entity: Node) -> void:
|
||||
_on_died(entity)
|
||||
|
||||
func _add(enemy: Node, player: Node, amount: float) -> void:
|
||||
if not enemy in aggro:
|
||||
aggro[enemy] = {}
|
||||
aggro[enemy][player] = aggro[enemy].get(player, 0.0) + amount
|
||||
|
||||
func _spread(enemy: Node, player: Node, amount: float) -> void:
|
||||
for other in aggro.keys():
|
||||
if other == enemy or not is_instance_valid(other):
|
||||
continue
|
||||
if (other as Node3D).global_position.distance_to((enemy as Node3D).global_position) <= 10.0:
|
||||
_add(other, player, amount)
|
||||
|
||||
func _top_target(enemy: Node) -> Node:
|
||||
if not enemy in aggro:
|
||||
return null
|
||||
var best: Node = null
|
||||
var best_v: float = -1.0
|
||||
for p in aggro[enemy].keys():
|
||||
if not is_instance_valid(p):
|
||||
continue
|
||||
if aggro[enemy][p] > best_v:
|
||||
best_v = aggro[enemy][p]
|
||||
best = p
|
||||
return best
|
||||
|
||||
func target_for(enemy: Node) -> Node:
|
||||
return _top_target(enemy)
|
||||
1
systems/aggro_system.gd.uid
Normal file
1
systems/aggro_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://6mabhel6qdl4
|
||||
@@ -1,121 +0,0 @@
|
||||
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]
|
||||
if entity.is_in_group("invasion"):
|
||||
_force_invasion_target(entity, data)
|
||||
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 _force_invasion_target(entity: Node, data: Dictionary) -> void:
|
||||
var tavern: Node = get_tree().get_first_node_in_group("tavern")
|
||||
if not tavern:
|
||||
return
|
||||
data["target"] = tavern
|
||||
if data["state"] == State.IDLE or data["state"] == State.RETURN:
|
||||
data["state"] = State.CHASE
|
||||
|
||||
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
|
||||
if data["target"].is_in_group("tavern"):
|
||||
attack_range += 3.0
|
||||
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
|
||||
_face_target(entity, data["target"])
|
||||
|
||||
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 attack_range: float = base.attack_range
|
||||
if data["target"].is_in_group("tavern"):
|
||||
attack_range += 3.0
|
||||
var dist: float = entity.global_position.distance_to(data["target"].global_position)
|
||||
if dist > attack_range:
|
||||
data["state"] = State.CHASE
|
||||
return
|
||||
if data["attack_timer"] <= 0:
|
||||
data["attack_timer"] = base.attack_cooldown
|
||||
var scale: float = data.get("scale", 1.0)
|
||||
EventBus.damage_requested.emit(entity, data["target"], base.attack_damage * scale)
|
||||
EventBus.attack_executed.emit(entity, entity.global_position, -entity.global_transform.basis.z, base.attack_damage * scale)
|
||||
entity.velocity.x = 0
|
||||
entity.velocity.z = 0
|
||||
_face_target(entity, data["target"])
|
||||
|
||||
func _face_target(entity: Node3D, target: Node3D) -> void:
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
var to_target: Vector3 = target.global_position - entity.global_position
|
||||
to_target.y = 0
|
||||
if to_target.length() < 0.01:
|
||||
return
|
||||
var yaw: float = atan2(-to_target.x, -to_target.z)
|
||||
var delta: float = get_physics_process_delta_time()
|
||||
entity.rotation.y = lerp_angle(entity.rotation.y, yaw, 8.0 * delta)
|
||||
|
||||
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 +0,0 @@
|
||||
uid://dokr1ut7ea541
|
||||
@@ -1,27 +0,0 @@
|
||||
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) * PlayerData.level_scale
|
||||
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 +0,0 @@
|
||||
uid://dvuds0uuffj6t
|
||||
@@ -1,113 +1,80 @@
|
||||
extends Node
|
||||
|
||||
const SFX_PATHS := {
|
||||
"hit": "res://assets/audio/sfx/hit.wav",
|
||||
"death": "res://assets/audio/sfx/death.wav",
|
||||
"level_up": "res://assets/audio/sfx/level_up.wav",
|
||||
"ability_cast": "res://assets/audio/sfx/ability_cast.wav",
|
||||
"portal_spawn": "res://assets/audio/sfx/portal_spawn.wav",
|
||||
"invasion_alarm": "res://assets/audio/sfx/invasion_alarm.wav",
|
||||
"tavern_damage": "res://assets/audio/sfx/tavern_damage.wav",
|
||||
}
|
||||
const SND_HIT: AudioStream = preload("res://assets/audio/sfx/hit.wav")
|
||||
const SND_DEATH: AudioStream = preload("res://assets/audio/sfx/death.wav")
|
||||
const SND_ABILITY: AudioStream = preload("res://assets/audio/sfx/ability_cast.wav")
|
||||
const SND_PORTAL: AudioStream = preload("res://assets/audio/sfx/portal_spawn.wav")
|
||||
const SND_LEVEL: AudioStream = preload("res://assets/audio/sfx/level_up.wav")
|
||||
const SND_INVASION: AudioStream = preload("res://assets/audio/sfx/invasion_alarm.wav")
|
||||
const SND_VILLAGE: AudioStream = preload("res://assets/audio/sfx/tavern_damage.wav")
|
||||
const MUS_TAVERN: AudioStream = preload("res://assets/audio/music/tavern.wav")
|
||||
const MUS_BATTLE: AudioStream = preload("res://assets/audio/music/battle.wav")
|
||||
const MUS_INVASION: AudioStream = preload("res://assets/audio/music/invasion.wav")
|
||||
|
||||
const MUSIC_PATHS := {
|
||||
"tavern": "res://assets/audio/music/tavern.wav",
|
||||
"battle": "res://assets/audio/music/battle.wav",
|
||||
"invasion": "res://assets/audio/music/invasion.wav",
|
||||
}
|
||||
|
||||
const SFX_POOL_SIZE := 8
|
||||
|
||||
var sfx_cache: Dictionary = {}
|
||||
var music_cache: Dictionary = {}
|
||||
var sfx_players: Array[AudioStreamPlayer] = []
|
||||
var music_player: AudioStreamPlayer = null
|
||||
var current_music: String = ""
|
||||
var music_player: AudioStreamPlayer
|
||||
|
||||
func _ready() -> void:
|
||||
for i in range(SFX_POOL_SIZE):
|
||||
var p := AudioStreamPlayer.new()
|
||||
p.volume_db = -6.0
|
||||
add_child(p)
|
||||
sfx_players.append(p)
|
||||
music_player = AudioStreamPlayer.new()
|
||||
music_player.volume_db = -12.0
|
||||
music_player.finished.connect(_on_music_finished)
|
||||
music_player.bus = "Master"
|
||||
music_player.volume_db = -10.0
|
||||
add_child(music_player)
|
||||
_preload_audio()
|
||||
EventBus.attack_executed.connect(_on_attack_executed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.damage_dealt.connect(_on_damage_dealt)
|
||||
EventBus.entity_died.connect(_on_died)
|
||||
EventBus.ability_used.connect(_on_ability_used)
|
||||
EventBus.portal_spawned.connect(_on_portal_spawned)
|
||||
EventBus.level_up.connect(_on_level_up)
|
||||
EventBus.portal_spawn.connect(_on_portal_spawn)
|
||||
EventBus.tavern_damaged.connect(_on_tavern_damaged)
|
||||
EventBus.invasion_started.connect(_on_invasion_started)
|
||||
EventBus.invasion_ended.connect(_on_invasion_ended)
|
||||
EventBus.village_damaged.connect(_on_village_damaged)
|
||||
EventBus.wave_started.connect(_on_wave_started)
|
||||
_play_music("tavern")
|
||||
call_deferred("_play_music", MUS_TAVERN)
|
||||
|
||||
func _preload_audio() -> void:
|
||||
for key in SFX_PATHS:
|
||||
var path: String = SFX_PATHS[key]
|
||||
if ResourceLoader.exists(path):
|
||||
var stream: AudioStream = load(path)
|
||||
if stream:
|
||||
sfx_cache[key] = stream
|
||||
for key in MUSIC_PATHS:
|
||||
var path: String = MUSIC_PATHS[key]
|
||||
if ResourceLoader.exists(path):
|
||||
var stream: AudioStream = load(path)
|
||||
if stream is AudioStreamWAV:
|
||||
(stream as AudioStreamWAV).loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
(stream as AudioStreamWAV).loop_end = (stream as AudioStreamWAV).data.size() / 2
|
||||
if stream:
|
||||
music_cache[key] = stream
|
||||
func _on_damage_dealt(_a: Node, target: Node, _amount: float) -> void:
|
||||
_play_at(target, SND_HIT, -8.0)
|
||||
|
||||
func play_sfx(key: String) -> void:
|
||||
if not sfx_cache.has(key):
|
||||
return
|
||||
for p in sfx_players:
|
||||
if not p.playing:
|
||||
p.stream = sfx_cache[key]
|
||||
p.play()
|
||||
return
|
||||
func _on_died(entity: Node) -> void:
|
||||
_play_at(entity, SND_DEATH, -4.0)
|
||||
|
||||
func _play_music(key: String) -> void:
|
||||
if current_music == key and music_player.playing:
|
||||
func _on_ability_used(player: Node, _i: int, _a: Resource) -> void:
|
||||
_play_at(player, SND_ABILITY, -10.0)
|
||||
|
||||
func _on_portal_spawned(p: Node) -> void:
|
||||
_play_at(p, SND_PORTAL, -4.0)
|
||||
|
||||
func _on_level_up(_p: Node, _l: int) -> void:
|
||||
_play_global(SND_LEVEL, -4.0)
|
||||
|
||||
func _on_invasion_started() -> void:
|
||||
_play_global(SND_INVASION, -2.0)
|
||||
_play_music(MUS_INVASION)
|
||||
|
||||
func _on_village_damaged(_c: float, _m: float) -> void:
|
||||
_play_global(SND_VILLAGE, -8.0)
|
||||
|
||||
func _on_wave_started(_n: int) -> void:
|
||||
_play_music(MUS_BATTLE)
|
||||
|
||||
func _play_at(node: Node, stream: AudioStream, vol: float) -> void:
|
||||
if not is_instance_valid(node) or not (node is Node3D):
|
||||
return
|
||||
if not music_cache.has(key):
|
||||
var p := AudioStreamPlayer3D.new()
|
||||
p.stream = stream
|
||||
p.volume_db = vol
|
||||
p.max_distance = 30.0
|
||||
get_tree().current_scene.add_child(p)
|
||||
p.global_position = (node as Node3D).global_position
|
||||
p.play()
|
||||
p.finished.connect(func(): p.queue_free())
|
||||
|
||||
func _play_global(stream: AudioStream, vol: float) -> void:
|
||||
var p := AudioStreamPlayer.new()
|
||||
p.stream = stream
|
||||
p.volume_db = vol
|
||||
add_child(p)
|
||||
p.play()
|
||||
p.finished.connect(func(): p.queue_free())
|
||||
|
||||
func _play_music(stream: AudioStream) -> void:
|
||||
if music_player.stream == stream and music_player.playing:
|
||||
return
|
||||
music_player.stream = music_cache[key]
|
||||
music_player.stream = stream
|
||||
music_player.play()
|
||||
current_music = key
|
||||
|
||||
func _on_music_finished() -> void:
|
||||
if current_music != "" and music_cache.has(current_music):
|
||||
music_player.play()
|
||||
|
||||
func _on_attack_executed(_attacker, _pos, _dir, _dmg) -> void:
|
||||
play_sfx("hit")
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity == PlayerData:
|
||||
return
|
||||
play_sfx("death")
|
||||
|
||||
func _on_level_up(_player, _level) -> void:
|
||||
play_sfx("level_up")
|
||||
|
||||
func _on_portal_spawn(_portal, _enemies) -> void:
|
||||
play_sfx("portal_spawn")
|
||||
|
||||
func _on_tavern_damaged(_current, _max_val) -> void:
|
||||
play_sfx("tavern_damage")
|
||||
|
||||
func _on_invasion_started(_enemies) -> void:
|
||||
play_sfx("invasion_alarm")
|
||||
_play_music("invasion")
|
||||
|
||||
func _on_invasion_ended(_success) -> void:
|
||||
_play_music("battle")
|
||||
|
||||
func _on_wave_started(_wave) -> void:
|
||||
if current_music != "invasion":
|
||||
_play_music("battle")
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://cbfc1ys0i4svm
|
||||
uid://h2qcbg7iv2yn
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://b17o3hfdm8uo6
|
||||
@@ -1,142 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://y2bm5ssu77wp
|
||||
92
systems/building_system.gd
Normal file
92
systems/building_system.gd
Normal file
@@ -0,0 +1,92 @@
|
||||
extends Node
|
||||
|
||||
const BUILDING_SCENE: PackedScene = preload("res://scenes/entities/building/building.tscn")
|
||||
const GRID_SIZE: float = 1.0
|
||||
|
||||
@onready var inventory_system: Node = get_node("../InventorySystem")
|
||||
|
||||
var blueprints: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
blueprints = [
|
||||
{"id": &"floor", "name": "Floor", "size": Vector3(1, 0.2, 1), "color": Color(0.55, 0.4, 0.25), "material": &"wood", "cost": 1},
|
||||
{"id": &"wall", "name": "Wall", "size": Vector3(1, 2, 0.2), "color": Color(0.7, 0.6, 0.45), "material": &"wood", "cost": 1},
|
||||
{"id": &"door", "name": "Door", "size": Vector3(1, 2, 0.15), "color": Color(0.4, 0.25, 0.15), "material": &"wood", "cost": 2},
|
||||
{"id": &"roof", "name": "Roof", "size": Vector3(1, 0.2, 1), "color": Color(0.5, 0.3, 0.2), "material": &"wood", "cost": 1}
|
||||
]
|
||||
|
||||
func _building_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Buildings")
|
||||
if n == null:
|
||||
n = get_node_or_null("/root/Dungeon/EntityRoot/Buildings")
|
||||
return n
|
||||
|
||||
func get_blueprints() -> Array:
|
||||
return blueprints
|
||||
|
||||
func find_blueprint(id: StringName) -> Dictionary:
|
||||
for b in blueprints:
|
||||
if b.id == id:
|
||||
return b
|
||||
return {}
|
||||
|
||||
func snap_position(pos: Vector3) -> Vector3:
|
||||
return Vector3(round(pos.x / GRID_SIZE) * GRID_SIZE, max(0.0, pos.y), round(pos.z / GRID_SIZE) * GRID_SIZE)
|
||||
|
||||
func place(player: Node, id: StringName, pos: Vector3, rot: float) -> bool:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
request_place.rpc_id(1, player.get_path(), id, pos, rot)
|
||||
return false
|
||||
var bp: Dictionary = find_blueprint(id)
|
||||
if bp.is_empty():
|
||||
return false
|
||||
var mat: StringName = bp.material
|
||||
var cost: int = bp.cost
|
||||
if inventory_system.get_amount(player, mat) < cost:
|
||||
return false
|
||||
inventory_system.remove_item(player, mat, cost)
|
||||
var root := _building_root()
|
||||
if root == null:
|
||||
return false
|
||||
var b: StaticBody3D = BUILDING_SCENE.instantiate()
|
||||
b.building_id = id
|
||||
b.name = "B_%s_%d" % [str(id), Time.get_ticks_msec() + randi() % 1000]
|
||||
root.add_child(b, true)
|
||||
b.global_position = snap_position(pos)
|
||||
b.rotation.y = rot
|
||||
b.apply_building(id)
|
||||
_apply_building_visual.rpc(b.get_path(), id)
|
||||
EventBus.building_placed.emit(b)
|
||||
return true
|
||||
|
||||
func remove(player: Node, b_path: NodePath) -> bool:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
request_remove.rpc_id(1, player.get_path(), b_path)
|
||||
return false
|
||||
var node: Node = get_node_or_null(b_path)
|
||||
if node == null:
|
||||
return false
|
||||
var bp: Dictionary = find_blueprint(node.building_id)
|
||||
if not bp.is_empty():
|
||||
inventory_system.add_item(player, bp.material, max(1, int(float(bp.cost) * 0.5)))
|
||||
EventBus.building_removed.emit(node)
|
||||
node.queue_free()
|
||||
return true
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_place(player_path: NodePath, id: StringName, pos: Vector3, rot: float) -> void:
|
||||
var p: Node = get_node_or_null(player_path)
|
||||
if p:
|
||||
place(p, id, pos, rot)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_remove(player_path: NodePath, b_path: NodePath) -> void:
|
||||
var p: Node = get_node_or_null(player_path)
|
||||
if p:
|
||||
remove(p, b_path)
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _apply_building_visual(b_path: NodePath, id: StringName) -> void:
|
||||
var node: Node = get_node_or_null(b_path)
|
||||
if node and node.has_method("apply_building"):
|
||||
node.apply_building(id)
|
||||
1
systems/building_system.gd.uid
Normal file
1
systems/building_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://wt6sjhxolm4g
|
||||
16
systems/chat_system.gd
Normal file
16
systems/chat_system.gd
Normal file
@@ -0,0 +1,16 @@
|
||||
extends Node
|
||||
|
||||
func send(text: String) -> void:
|
||||
var msg: String = text.strip_edges()
|
||||
if msg == "":
|
||||
return
|
||||
var sender: String = Net.local_name
|
||||
var id: int = Net.local_id()
|
||||
if multiplayer.multiplayer_peer == null:
|
||||
EventBus.chat_message.emit(id, sender, msg)
|
||||
return
|
||||
_broadcast.rpc(id, sender, msg)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _broadcast(peer_id: int, sender: String, msg: String) -> void:
|
||||
EventBus.chat_message.emit(peer_id, sender, msg)
|
||||
1
systems/chat_system.gd.uid
Normal file
1
systems/chat_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c035shi1tlmql
|
||||
166
systems/combat/ability_system.gd
Normal file
166
systems/combat/ability_system.gd
Normal file
@@ -0,0 +1,166 @@
|
||||
extends Node
|
||||
|
||||
@onready var role_system: Node = get_node("../RoleSystem")
|
||||
@onready var cooldown_system: Node = get_node("../CooldownSystem")
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.ability_use_requested.connect(_on_ability_request)
|
||||
|
||||
func _on_ability_request(player: Node, index: int) -> void:
|
||||
if not is_instance_valid(player) or not Stats.has(player):
|
||||
return
|
||||
var target_str: String = ""
|
||||
var tv: Variant = (player as Node).get("current_target")
|
||||
if tv != null and is_instance_valid(tv) and tv is Node:
|
||||
target_str = String((tv as Node).get_path())
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
_request_use.rpc_id(1, String(player.get_path()), index, target_str)
|
||||
return
|
||||
_execute(player, index, target_str)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _request_use(path_str: String, index: int, target_str: String) -> void:
|
||||
var p: Node = get_node_or_null(NodePath(path_str))
|
||||
if p == null:
|
||||
return
|
||||
if target_str != "":
|
||||
var t := get_node_or_null(NodePath(target_str))
|
||||
if t and Stats.has(t):
|
||||
p.set("current_target", t)
|
||||
_execute(p, index, target_str)
|
||||
|
||||
func _execute(player: Node, index: int, _target_str: String = "") -> void:
|
||||
if index < 0 or index > 3:
|
||||
return
|
||||
var role: int = int(Stats.get_stat(player, "role", GameState.ROLE_DAMAGE))
|
||||
var set: AbilitySet = role_system.get_set(role)
|
||||
if set == null or index >= set.abilities.size():
|
||||
return
|
||||
var ability: Ability = set.abilities[index]
|
||||
if ability == null:
|
||||
return
|
||||
if not cooldown_system.is_ready(player, index):
|
||||
return
|
||||
if ability.uses_gcd and not cooldown_system.is_gcd_ready(player):
|
||||
return
|
||||
cooldown_system.set_cooldown(player, index, ability.cooldown, float(Stats.get_stat(player, "gcd_time", 0.5)) if ability.uses_gcd else 0.0)
|
||||
EventBus.ability_used.emit(player, index, ability)
|
||||
_apply_ability(player, ability)
|
||||
|
||||
func _apply_ability(player: Node, ability: Ability) -> void:
|
||||
match ability.type:
|
||||
Ability.Type.SINGLE:
|
||||
_apply_single(player, ability)
|
||||
Ability.Type.AOE:
|
||||
_apply_aoe(player, ability)
|
||||
Ability.Type.UTILITY:
|
||||
_apply_utility(player, ability)
|
||||
Ability.Type.ULT:
|
||||
_apply_ult(player, ability)
|
||||
_:
|
||||
pass
|
||||
|
||||
func _apply_single(player: Node, ability: Ability) -> void:
|
||||
var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
|
||||
var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0))
|
||||
var target: Node = _resolve_target(player, ability)
|
||||
if target == null:
|
||||
return
|
||||
if (player as Node3D).global_position.distance_to((target as Node3D).global_position) > ability.ability_range:
|
||||
return
|
||||
if ability.is_heal:
|
||||
EventBus.heal_requested.emit(player, target, ability.damage * heal_mult)
|
||||
else:
|
||||
EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element)
|
||||
if ability.shield_value > 0.0:
|
||||
_apply_shield(player, ability.shield_value)
|
||||
|
||||
func _apply_aoe(player: Node, ability: Ability) -> void:
|
||||
var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
|
||||
var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0))
|
||||
var origin: Vector3 = (player as Node3D).global_position
|
||||
var center: Vector3 = origin
|
||||
var target: Node = _resolve_target(player, ability)
|
||||
if target and (target as Node3D).global_position.distance_to(origin) <= ability.ability_range:
|
||||
center = (target as Node3D).global_position
|
||||
if ability.is_heal:
|
||||
for ally in get_tree().get_nodes_in_group("player"):
|
||||
if (ally as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
|
||||
EventBus.heal_requested.emit(player, ally, ability.damage * heal_mult)
|
||||
else:
|
||||
var hits: int = 0
|
||||
for foe in Stats.entities_in_group(&"enemies"):
|
||||
if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
|
||||
EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
|
||||
hits += 1
|
||||
for foe in Stats.entities_in_group(&"portals"):
|
||||
if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
|
||||
EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
|
||||
hits += 1
|
||||
for foe in Stats.entities_in_group(&"gates"):
|
||||
if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
|
||||
EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
|
||||
hits += 1
|
||||
if ability.shield_value > 0.0:
|
||||
_apply_shield(player, ability.shield_value * hits)
|
||||
|
||||
func _apply_utility(player: Node, ability: Ability) -> void:
|
||||
if ability.shield_multiplier > 0.0:
|
||||
var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
|
||||
var add: float = max_shield * ability.shield_multiplier
|
||||
Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + add))
|
||||
EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)
|
||||
|
||||
func _apply_ult(player: Node, ability: Ability) -> void:
|
||||
if ability.is_heal:
|
||||
_apply_aoe(player, ability)
|
||||
elif ability.shield_multiplier > 0.0:
|
||||
var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
|
||||
Stats.set_stat(player, "shield", min(max_shield * (1.0 + ability.shield_multiplier), float(Stats.get_stat(player, "shield", 0.0)) + max_shield * ability.shield_multiplier))
|
||||
EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)
|
||||
else:
|
||||
var target: Node = _resolve_target(player, ability)
|
||||
if target == null:
|
||||
return
|
||||
var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
|
||||
EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element)
|
||||
if ability.aoe_radius > 0.0:
|
||||
var center: Vector3 = (target as Node3D).global_position
|
||||
for foe in Stats.entities_in_group(&"enemies"):
|
||||
if foe == target:
|
||||
continue
|
||||
if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
|
||||
EventBus.damage_requested.emit(player, foe, ability.damage * 0.5 * dmg_mult, ability.element)
|
||||
|
||||
func _apply_shield(player: Node, amount: float) -> void:
|
||||
var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
|
||||
Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + amount))
|
||||
EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)
|
||||
|
||||
func _resolve_target(player: Node, ability: Ability) -> Node:
|
||||
if ability.is_heal:
|
||||
var lowest: Node = null
|
||||
var lowest_pct: float = 2.0
|
||||
for ally in get_tree().get_nodes_in_group("player"):
|
||||
if not Stats.has(ally):
|
||||
continue
|
||||
var hp: float = float(Stats.get_stat(ally, "health", 0.0))
|
||||
var max_hp: float = float(Stats.get_stat(ally, "max_health", 1.0))
|
||||
if max_hp <= 0.0 or hp <= 0.0:
|
||||
continue
|
||||
var pct: float = hp / max_hp
|
||||
if pct < lowest_pct:
|
||||
lowest = ally
|
||||
lowest_pct = pct
|
||||
return lowest
|
||||
var tv: Variant = (player as Node).get("current_target")
|
||||
if tv != null and is_instance_valid(tv) and tv is Node and Stats.has(tv):
|
||||
return tv as Node
|
||||
var nearest: Node = null
|
||||
var nearest_dist: float = INF
|
||||
for foe in Stats.entities_in_group(&"enemies") + Stats.entities_in_group(&"portals") + Stats.entities_in_group(&"gates"):
|
||||
var d: float = (foe as Node3D).global_position.distance_to((player as Node3D).global_position)
|
||||
if d < nearest_dist and d <= ability.ability_range:
|
||||
nearest = foe
|
||||
nearest_dist = d
|
||||
return nearest
|
||||
1
systems/combat/ability_system.gd.uid
Normal file
1
systems/combat/ability_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://e47e8o4ofgtg
|
||||
74
systems/combat/auto_attack_system.gd
Normal file
74
systems/combat/auto_attack_system.gd
Normal file
@@ -0,0 +1,74 @@
|
||||
extends Node
|
||||
|
||||
@onready var role_system: Node = get_node("../RoleSystem")
|
||||
@onready var cooldown_system: Node = get_node("../CooldownSystem")
|
||||
|
||||
var _accum: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
_accum += delta
|
||||
if _accum < 0.10:
|
||||
return
|
||||
_accum = 0.0
|
||||
for player in get_tree().get_nodes_in_group("player"):
|
||||
if not Stats.has(player):
|
||||
continue
|
||||
if int(Stats.get_stat(player, "health", 0.0)) <= 0:
|
||||
continue
|
||||
if not cooldown_system.is_aa_ready(player):
|
||||
continue
|
||||
var role: int = int(Stats.get_stat(player, "role", GameState.ROLE_DAMAGE))
|
||||
var set: AbilitySet = role_system.get_set(role)
|
||||
if set == null:
|
||||
continue
|
||||
var target: Node = _pick_target(player, set)
|
||||
if target == null:
|
||||
continue
|
||||
var dist: float = (player as Node3D).global_position.distance_to((target as Node3D).global_position)
|
||||
if dist > set.aa_range:
|
||||
continue
|
||||
var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
|
||||
var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0))
|
||||
if set.aa_is_heal:
|
||||
EventBus.heal_requested.emit(player, target, set.aa_damage * heal_mult)
|
||||
else:
|
||||
EventBus.damage_requested.emit(player, target, set.aa_damage * dmg_mult, Element.NONE)
|
||||
cooldown_system.set_aa(player, float(Stats.get_stat(player, "aa_cooldown", 0.5)))
|
||||
|
||||
func _pick_target(player: Node, set: AbilitySet) -> Node:
|
||||
if set.aa_is_heal:
|
||||
var lowest: Node = null
|
||||
var lowest_pct: float = 2.0
|
||||
for ally in get_tree().get_nodes_in_group("player"):
|
||||
if not Stats.has(ally):
|
||||
continue
|
||||
var hp: float = float(Stats.get_stat(ally, "health", 0.0))
|
||||
var max_hp: float = float(Stats.get_stat(ally, "max_health", 1.0))
|
||||
if hp <= 0.0:
|
||||
continue
|
||||
var pct: float = hp / max_hp
|
||||
if pct < lowest_pct and (player as Node3D).global_position.distance_to((ally as Node3D).global_position) <= set.aa_range:
|
||||
lowest = ally
|
||||
lowest_pct = pct
|
||||
return lowest
|
||||
var current_var: Variant = (player as Node).get("current_target")
|
||||
if current_var != null and is_instance_valid(current_var) and current_var is Node and Stats.has(current_var):
|
||||
return current_var
|
||||
var nearest: Node = null
|
||||
var nearest_dist: float = INF
|
||||
for foe in Stats.entities_in_group(&"enemies"):
|
||||
if not is_instance_valid(foe):
|
||||
continue
|
||||
var d: float = (foe as Node3D).global_position.distance_to((player as Node3D).global_position)
|
||||
if d < nearest_dist and d <= set.aa_range:
|
||||
nearest = foe
|
||||
nearest_dist = d
|
||||
return nearest
|
||||
1
systems/combat/auto_attack_system.gd.uid
Normal file
1
systems/combat/auto_attack_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://comfiqsxl5t1e
|
||||
@@ -1,19 +1,80 @@
|
||||
extends Node
|
||||
|
||||
const ABILITY_COUNT: int = 4
|
||||
|
||||
var _cds: Dictionary = {}
|
||||
var _max_cds: Dictionary = {}
|
||||
var _gcd: Dictionary = {}
|
||||
var _aa: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_deregistered.connect(_on_dereg)
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
set_physics_process(true)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
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 register(entity: Node) -> void:
|
||||
_cds[entity] = PackedFloat32Array([0, 0, 0, 0])
|
||||
_max_cds[entity] = PackedFloat32Array([0, 0, 0, 0])
|
||||
_gcd[entity] = 0.0
|
||||
_aa[entity] = 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
|
||||
func _on_dereg(entity: Node) -> void:
|
||||
_cds.erase(entity)
|
||||
_max_cds.erase(entity)
|
||||
_gcd.erase(entity)
|
||||
_aa.erase(entity)
|
||||
|
||||
func _on_role_changed(player: Node, _role: int) -> void:
|
||||
if player in _cds:
|
||||
var z: PackedFloat32Array = PackedFloat32Array([0, 0, 0, 0])
|
||||
_cds[player] = z
|
||||
_max_cds[player] = z.duplicate()
|
||||
_gcd[player] = 0.0
|
||||
_aa[player] = 0.0
|
||||
|
||||
func is_ready(entity: Node, index: int) -> bool:
|
||||
if not entity in _cds:
|
||||
return true
|
||||
if index < 0 or index >= ABILITY_COUNT:
|
||||
return false
|
||||
return _cds[entity][index] <= 0.0
|
||||
|
||||
func is_gcd_ready(entity: Node) -> bool:
|
||||
return _gcd.get(entity, 0.0) <= 0.0
|
||||
|
||||
func is_aa_ready(entity: Node) -> bool:
|
||||
return _aa.get(entity, 0.0) <= 0.0
|
||||
|
||||
func set_cooldown(entity: Node, index: int, cd: float, gcd: float = 0.0) -> void:
|
||||
if not entity in _cds:
|
||||
register(entity)
|
||||
_cds[entity][index] = cd
|
||||
_max_cds[entity][index] = cd
|
||||
if gcd > 0.0:
|
||||
_gcd[entity] = gcd
|
||||
|
||||
func set_aa(entity: Node, value: float) -> void:
|
||||
_aa[entity] = value
|
||||
|
||||
var _emit_accum: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
for entity in _cds.keys():
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var arr: PackedFloat32Array = _cds[entity]
|
||||
for i in range(arr.size()):
|
||||
if arr[i] > 0.0:
|
||||
arr[i] = max(0.0, arr[i] - delta)
|
||||
_cds[entity] = arr
|
||||
if _gcd.get(entity, 0.0) > 0.0:
|
||||
_gcd[entity] = max(0.0, _gcd[entity] - delta)
|
||||
if _aa.get(entity, 0.0) > 0.0:
|
||||
_aa[entity] = max(0.0, _aa[entity] - delta)
|
||||
_emit_accum += delta
|
||||
if _emit_accum < 0.10:
|
||||
return
|
||||
_emit_accum = 0.0
|
||||
for entity in _cds.keys():
|
||||
if is_instance_valid(entity) and entity.is_in_group("player") and entity.is_multiplayer_authority():
|
||||
EventBus.cooldown_tick.emit(entity, _cds[entity], _max_cds[entity], _gcd.get(entity, 0.0))
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://ddos7mo8rahou
|
||||
uid://cr6jycuq6udgf
|
||||
|
||||
49
systems/crafting_system.gd
Normal file
49
systems/crafting_system.gd
Normal file
@@ -0,0 +1,49 @@
|
||||
extends Node
|
||||
|
||||
@onready var inventory_system: Node = get_node("../InventorySystem")
|
||||
|
||||
var recipes: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
recipes = [
|
||||
{"id": &"wood", "name": "Wood", "out_count": 1, "inputs": {&"essence": 1}},
|
||||
{"id": &"stone", "name": "Stone", "out_count": 1, "inputs": {&"essence": 2}},
|
||||
{"id": &"iron", "name": "Iron", "out_count": 1, "inputs": {&"essence": 5}},
|
||||
{"id": &"sword", "name": "Sword", "out_count": 1, "inputs": {&"iron": 3, &"wood": 1}},
|
||||
]
|
||||
|
||||
func get_recipes() -> Array:
|
||||
return recipes
|
||||
|
||||
func can_craft(player: Node, recipe: Dictionary) -> bool:
|
||||
for input_id in recipe.inputs.keys():
|
||||
if inventory_system.get_amount(player, input_id) < recipe.inputs[input_id]:
|
||||
return false
|
||||
return true
|
||||
|
||||
func craft(player: Node, recipe_id: StringName) -> bool:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
request_craft.rpc_id(1, player.get_path(), recipe_id)
|
||||
return false
|
||||
var recipe: Dictionary = _find_recipe(recipe_id)
|
||||
if recipe.is_empty():
|
||||
return false
|
||||
if not can_craft(player, recipe):
|
||||
return false
|
||||
for input_id in recipe.inputs.keys():
|
||||
inventory_system.remove_item(player, input_id, recipe.inputs[input_id])
|
||||
inventory_system.add_item(player, recipe.id, recipe.out_count)
|
||||
EventBus.item_crafted.emit(player, recipe.id)
|
||||
return true
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_craft(path: NodePath, recipe_id: StringName) -> void:
|
||||
var player: Node = get_node_or_null(path)
|
||||
if player:
|
||||
craft(player, recipe_id)
|
||||
|
||||
func _find_recipe(id: StringName) -> Dictionary:
|
||||
for r in recipes:
|
||||
if r.id == id:
|
||||
return r
|
||||
return {}
|
||||
1
systems/crafting_system.gd.uid
Normal file
1
systems/crafting_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c4rcp5bw5rjr1
|
||||
@@ -1,44 +0,0 @@
|
||||
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
|
||||
if not target.is_in_group("tavern"):
|
||||
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)
|
||||
elif entity.is_in_group("tavern"):
|
||||
var health: float = TavernData.get_stat(entity, "health") - amount
|
||||
if health < 0:
|
||||
health = 0
|
||||
TavernData.set_health(entity, health)
|
||||
|
||||
func _get_player() -> Node:
|
||||
return get_tree().get_first_node_in_group("player")
|
||||
@@ -1 +0,0 @@
|
||||
uid://cmy1kqo1pk1q8
|
||||
@@ -1,65 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://ce12ledregjqx
|
||||
92
systems/dialog_system.gd
Normal file
92
systems/dialog_system.gd
Normal file
@@ -0,0 +1,92 @@
|
||||
extends Node
|
||||
|
||||
const OLLAMA_URL: String = "http://127.0.0.1:11434/api/generate"
|
||||
const DEFAULT_MODEL: String = "qwen2.5:0.5b"
|
||||
const REQUEST_TIMEOUT: float = 25.0
|
||||
|
||||
var _http: HTTPRequest
|
||||
var _pending: Dictionary = {}
|
||||
var _pending_target: Node = null
|
||||
|
||||
func _ready() -> void:
|
||||
_http = HTTPRequest.new()
|
||||
_http.timeout = REQUEST_TIMEOUT
|
||||
add_child(_http)
|
||||
_http.request_completed.connect(_on_response)
|
||||
|
||||
func ask(npc: Node, player: Node, question: String) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
request_ask.rpc_id(1, npc.get_path(), player.get_path(), question)
|
||||
return
|
||||
var profile: NpcProfile = npc.profile
|
||||
var system_prompt: String = "Du bist %s, eine NPC in einem mittelalterlichen Dorf. Lore: %s. Persönlichkeit: %s. Antworte knapp (max 2 Sätze) auf Deutsch und bleibe immer in deiner Rolle. Erfinde keine Fakten über die Welt." % [profile.display_name, profile.lore, profile.personality]
|
||||
var prompt: String = "Spieler: %s\n%s:" % [question, profile.display_name]
|
||||
_send_request(npc, player, system_prompt, prompt)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_ask(npc_path: NodePath, player_path: NodePath, question: String) -> void:
|
||||
var npc: Node = get_node_or_null(npc_path)
|
||||
var player: Node = get_node_or_null(player_path)
|
||||
if npc and player:
|
||||
ask(npc, player, question)
|
||||
|
||||
func _send_request(npc: Node, player: Node, system_prompt: String, prompt: String) -> void:
|
||||
var body: Dictionary = {
|
||||
"model": DEFAULT_MODEL,
|
||||
"prompt": prompt,
|
||||
"system": system_prompt,
|
||||
"stream": false,
|
||||
"options": {"temperature": 0.7, "num_predict": 120}
|
||||
}
|
||||
var headers := PackedStringArray(["Content-Type: application/json"])
|
||||
var json := JSON.stringify(body)
|
||||
var token: int = randi()
|
||||
_pending[token] = {"npc": npc, "player": player}
|
||||
_pending_target = player
|
||||
var err := _http.request(OLLAMA_URL, headers, HTTPClient.METHOD_POST, json)
|
||||
if err != OK:
|
||||
_send_fallback(npc, player)
|
||||
|
||||
func _on_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var entry_keys: Array = _pending.keys()
|
||||
if entry_keys.is_empty():
|
||||
return
|
||||
var token: int = entry_keys[0]
|
||||
var entry: Dictionary = _pending[token]
|
||||
_pending.erase(token)
|
||||
var npc: Node = entry.npc
|
||||
var player: Node = entry.player
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
|
||||
_send_fallback(npc, player)
|
||||
return
|
||||
var text: String = body.get_string_from_utf8()
|
||||
var data: Variant = JSON.parse_string(text)
|
||||
if typeof(data) != TYPE_DICTIONARY or not "response" in data:
|
||||
_send_fallback(npc, player)
|
||||
return
|
||||
var answer: String = (data.response as String).strip_edges()
|
||||
if answer == "":
|
||||
_send_fallback(npc, player)
|
||||
return
|
||||
_deliver(player, npc, answer)
|
||||
|
||||
func _send_fallback(npc: Node, player: Node) -> void:
|
||||
if not is_instance_valid(npc):
|
||||
return
|
||||
var profile: NpcProfile = npc.profile
|
||||
_deliver(player, npc, profile.fallback_text)
|
||||
|
||||
func _deliver(player: Node, npc: Node, text: String) -> void:
|
||||
if player == null or not is_instance_valid(player):
|
||||
return
|
||||
var auth: int = player.get_multiplayer_authority()
|
||||
_on_dialog_answer.rpc_id(auth, npc.get_path(), text)
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _on_dialog_answer(npc_path: NodePath, text: String) -> void:
|
||||
var npc: Node = get_node_or_null(npc_path)
|
||||
if npc:
|
||||
EventBus.chat_message.emit(0, npc.profile.display_name if npc.has_method("get") else "NPC", text)
|
||||
for hud in get_tree().get_nodes_in_group("dialog_ui"):
|
||||
if hud.has_method("show_answer"):
|
||||
hud.show_answer(text)
|
||||
1
systems/dialog_system.gd.uid
Normal file
1
systems/dialog_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brxmr4qquuu5o
|
||||
@@ -1,13 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://lc5n3uxi4fho
|
||||
@@ -1,14 +0,0 @@
|
||||
extends Resource
|
||||
class_name Effect
|
||||
|
||||
enum Type { BUFF, DEBUFF, AURA }
|
||||
|
||||
@export var effect_name: String = ""
|
||||
@export var type: Type = Type.BUFF
|
||||
@export var stat: String = ""
|
||||
@export var value: float = 0.0
|
||||
@export var duration: float = -1.0
|
||||
@export var is_multiplier: bool = true
|
||||
@export var aura_radius: float = 0.0
|
||||
@export var tick_interval: float = 0.0
|
||||
@export var element: int = 0
|
||||
@@ -1 +0,0 @@
|
||||
uid://djbni7iy5pw2m
|
||||
138
systems/effect_system.gd
Normal file
138
systems/effect_system.gd
Normal file
@@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
|
||||
var _active: Dictionary = {}
|
||||
var _passive_auras: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_died.connect(_on_died)
|
||||
EventBus.entity_deregistered.connect(_on_dereg)
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
var _heavy_accum: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
_tick_durations(delta)
|
||||
_heavy_accum += delta
|
||||
if _heavy_accum < 0.20:
|
||||
return
|
||||
_heavy_accum = 0.0
|
||||
_propagate_auras()
|
||||
_recompute_buffs()
|
||||
|
||||
func apply(target: Node, effect: Effect, source: Node = null) -> void:
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
if not target in _active:
|
||||
_active[target] = []
|
||||
for entry in _active[target]:
|
||||
if entry.effect.effect_name == effect.effect_name and entry.source == source:
|
||||
entry.remaining = effect.duration
|
||||
entry.tick_timer = effect.tick_interval
|
||||
EventBus.effect_applied.emit(target, effect, source)
|
||||
return
|
||||
_active[target].append({
|
||||
"effect": effect,
|
||||
"source": source,
|
||||
"remaining": effect.duration,
|
||||
"tick_timer": effect.tick_interval
|
||||
})
|
||||
EventBus.effect_applied.emit(target, effect, source)
|
||||
|
||||
func remove_by_source(target: Node, source: Node) -> void:
|
||||
if not target in _active:
|
||||
return
|
||||
var keep: Array = []
|
||||
for entry in _active[target]:
|
||||
if entry.source == source:
|
||||
EventBus.effect_expired.emit(target, entry.effect)
|
||||
else:
|
||||
keep.append(entry)
|
||||
_active[target] = keep
|
||||
|
||||
func apply_passive_aura(player: Node, ability: Ability) -> void:
|
||||
if ability == null or ability.passive_stat == &"":
|
||||
return
|
||||
_passive_auras[player] = ability
|
||||
for target in get_tree().get_nodes_in_group("player"):
|
||||
remove_by_source(target, player)
|
||||
|
||||
func _propagate_auras() -> void:
|
||||
for source in _passive_auras.keys():
|
||||
if not is_instance_valid(source):
|
||||
_passive_auras.erase(source)
|
||||
continue
|
||||
var ability: Ability = _passive_auras[source]
|
||||
var effect := Effect.new()
|
||||
effect.effect_name = StringName("aura_%s_%s" % [ability.passive_stat, source.name])
|
||||
effect.type = Effect.Type.AURA
|
||||
effect.stat = ability.passive_stat
|
||||
effect.value = ability.passive_value
|
||||
effect.is_multiplier = true
|
||||
effect.duration = 0.5
|
||||
effect.aura_radius = ability.passive_radius
|
||||
var src_pos: Vector3 = (source as Node3D).global_position
|
||||
for target in get_tree().get_nodes_in_group("player"):
|
||||
if not Stats.has(target):
|
||||
continue
|
||||
var d: float = (target as Node3D).global_position.distance_to(src_pos)
|
||||
if d <= ability.passive_radius:
|
||||
apply(target, effect, source)
|
||||
|
||||
func _tick_durations(delta: float) -> void:
|
||||
for target in _active.keys():
|
||||
if not is_instance_valid(target):
|
||||
_active.erase(target)
|
||||
continue
|
||||
var keep: Array = []
|
||||
for entry in _active[target]:
|
||||
if entry.effect.duration > 0.0:
|
||||
entry.remaining -= delta
|
||||
if entry.remaining <= 0.0:
|
||||
EventBus.effect_expired.emit(target, entry.effect)
|
||||
continue
|
||||
if entry.effect.tick_interval > 0.0:
|
||||
entry.tick_timer -= delta
|
||||
if entry.tick_timer <= 0.0:
|
||||
entry.tick_timer = entry.effect.tick_interval
|
||||
if entry.effect.type == Effect.Type.DOT:
|
||||
EventBus.damage_requested.emit(entry.source, target, entry.effect.value, entry.effect.element)
|
||||
elif entry.effect.type == Effect.Type.HOT:
|
||||
EventBus.heal_requested.emit(entry.source, target, entry.effect.value)
|
||||
keep.append(entry)
|
||||
_active[target] = keep
|
||||
|
||||
func _recompute_buffs() -> void:
|
||||
for target in _active.keys():
|
||||
if not is_instance_valid(target) or not Stats.has(target):
|
||||
continue
|
||||
var bonus: Dictionary = {}
|
||||
var mult: Dictionary = {}
|
||||
for entry in _active[target]:
|
||||
var e: Effect = entry.effect
|
||||
if e.stat == &"":
|
||||
continue
|
||||
if e.is_multiplier:
|
||||
mult[e.stat] = mult.get(e.stat, 0.0) + e.value
|
||||
else:
|
||||
bonus[e.stat] = bonus.get(e.stat, 0.0) + e.value
|
||||
for stat in [&"buff_damage", &"buff_heal", &"buff_shield"]:
|
||||
var base: float = 1.0
|
||||
var v: float = base + mult.get(stat, 0.0) + bonus.get(stat, 0.0)
|
||||
Stats.set_stat(target, stat, v)
|
||||
EventBus.buff_changed.emit(target, stat, v)
|
||||
|
||||
func _on_died(entity: Node) -> void:
|
||||
if entity in _active:
|
||||
for entry in _active[entity]:
|
||||
EventBus.effect_expired.emit(entity, entry.effect)
|
||||
_active.erase(entity)
|
||||
_passive_auras.erase(entity)
|
||||
|
||||
func _on_dereg(entity: Node) -> void:
|
||||
_active.erase(entity)
|
||||
_passive_auras.erase(entity)
|
||||
1
systems/effect_system.gd.uid
Normal file
1
systems/effect_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dai1m8j8conpk
|
||||
@@ -1,53 +1,38 @@
|
||||
extends Node
|
||||
|
||||
enum Element { NONE, FIRE }
|
||||
@onready var effect_system: Node = get_node("../EffectSystem")
|
||||
|
||||
var applied_elements: Dictionary = {}
|
||||
var _applied: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.element_damage_dealt.connect(_on_element_damage_dealt)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.effect_expired.connect(_on_effect_expired)
|
||||
EventBus.damage_requested.connect(_on_damage_requested)
|
||||
EventBus.entity_died.connect(_on_died)
|
||||
|
||||
func _on_element_damage_dealt(attacker: Node, target: Node, _amount: float, element: int) -> void:
|
||||
func _on_damage_requested(attacker: Node, target: Node, _amount: float, element: int) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
if element == Element.NONE:
|
||||
return
|
||||
if not target.is_in_group("enemies") and not target.is_in_group("portals"):
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
var current: int = applied_elements.get(target, Element.NONE)
|
||||
if current != Element.NONE and current != element:
|
||||
_trigger_reaction(attacker, target, current, element)
|
||||
if not (target.is_in_group("enemies") or target.is_in_group("portals") or target.is_in_group("gates")):
|
||||
return
|
||||
_apply_element(attacker, target, element)
|
||||
apply_element(attacker, target, element)
|
||||
|
||||
func _apply_element(source: Node, target: Node, element: int) -> void:
|
||||
applied_elements[target] = element
|
||||
func apply_element(source: Node, target: Node, element: int) -> void:
|
||||
if element == Element.NONE or not is_instance_valid(target):
|
||||
return
|
||||
_applied[target] = element
|
||||
EventBus.element_applied.emit(target, element)
|
||||
match element:
|
||||
Element.FIRE:
|
||||
_apply_fire(source, target)
|
||||
if element == Element.FIRE:
|
||||
var dot := Effect.new()
|
||||
dot.effect_name = &"fire_dot"
|
||||
dot.type = Effect.Type.DOT
|
||||
dot.value = 3.0
|
||||
dot.duration = 6.0
|
||||
dot.tick_interval = 2.0
|
||||
dot.element = Element.FIRE
|
||||
effect_system.apply(target, dot, source)
|
||||
|
||||
func _apply_fire(source: Node, target: Node) -> void:
|
||||
var fire_dot := Effect.new()
|
||||
fire_dot.effect_name = "Burning"
|
||||
fire_dot.type = Effect.Type.DEBUFF
|
||||
fire_dot.stat = "damage"
|
||||
fire_dot.value = 3.0
|
||||
fire_dot.duration = 6.0
|
||||
fire_dot.is_multiplier = false
|
||||
fire_dot.tick_interval = 2.0
|
||||
fire_dot.element = Element.FIRE
|
||||
EventBus.effect_requested.emit(target, fire_dot, source)
|
||||
|
||||
func _trigger_reaction(_attacker: Node, target: Node, _elem_a: int, _elem_b: int) -> void:
|
||||
applied_elements.erase(target)
|
||||
EventBus.element_reaction.emit(target, _elem_a, _elem_b, "")
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
applied_elements.erase(entity)
|
||||
|
||||
func _on_effect_expired(target: Node, effect: Effect) -> void:
|
||||
if effect.element != Element.NONE:
|
||||
var current: int = applied_elements.get(target, Element.NONE)
|
||||
if current == effect.element:
|
||||
applied_elements.erase(target)
|
||||
func _on_died(entity: Node) -> void:
|
||||
_applied.erase(entity)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://bqebxfvticxto
|
||||
uid://cndhsw6vikcd1
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://8jyik37e4tjw
|
||||
@@ -1,32 +1,69 @@
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
_emit_initial.call_deferred()
|
||||
EventBus.damage_requested.connect(_on_damage_requested)
|
||||
EventBus.heal_requested.connect(_on_heal_requested)
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_regen_player(delta)
|
||||
_regen_entities(delta, EnemyData.entities)
|
||||
_regen_entities(delta, BossData.entities)
|
||||
var _regen_accum: float = 0.0
|
||||
|
||||
func _regen_player(delta: float) -> void:
|
||||
if not PlayerData.alive or PlayerData.health_regen <= 0:
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
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:
|
||||
_regen_accum += delta
|
||||
if _regen_accum < 0.20:
|
||||
return
|
||||
var dt: float = _regen_accum
|
||||
_regen_accum = 0.0
|
||||
for entity in Stats.entities():
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var data: Dictionary = 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"])
|
||||
var hp: float = float(Stats.get_stat(entity, "health", 0.0))
|
||||
var max_hp: float = float(Stats.get_stat(entity, "max_health", 0.0))
|
||||
var regen: float = float(Stats.get_stat(entity, "health_regen", 0.0))
|
||||
if regen > 0.0 and hp > 0.0 and hp < max_hp:
|
||||
var new_hp: float = min(max_hp, hp + regen * dt)
|
||||
Stats.set_stat(entity, "health", new_hp)
|
||||
EventBus.health_changed.emit(entity, new_hp, max_hp)
|
||||
|
||||
func _emit_initial() -> void:
|
||||
EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health)
|
||||
EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield)
|
||||
func _on_damage_requested(attacker: Node, target: Node, amount: float, element: int) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
if not is_instance_valid(target) or not Stats.has(target):
|
||||
return
|
||||
var hp: float = float(Stats.get_stat(target, "health", 0.0))
|
||||
if hp <= 0.0:
|
||||
return
|
||||
var remaining := amount
|
||||
var shield: float = float(Stats.get_stat(target, "shield", 0.0))
|
||||
if shield > 0.0:
|
||||
var absorbed: float = min(shield, remaining)
|
||||
Stats.set_stat(target, "shield", shield - absorbed)
|
||||
Stats.set_stat(target, "shield_regen_timer", float(Stats.get_stat(target, "shield_regen_delay", 5.0)))
|
||||
remaining -= absorbed
|
||||
EventBus.shield_changed.emit(target, Stats.get_stat(target, "shield"), Stats.get_stat(target, "max_shield", 0.0))
|
||||
if shield - absorbed <= 0.0:
|
||||
EventBus.shield_broken.emit(target)
|
||||
if remaining > 0.0:
|
||||
var new_hp: float = max(0.0, hp - remaining)
|
||||
Stats.set_stat(target, "health", new_hp)
|
||||
EventBus.health_changed.emit(target, new_hp, Stats.get_stat(target, "max_health"))
|
||||
EventBus.damage_dealt.emit(attacker, target, amount)
|
||||
if new_hp <= 0.0:
|
||||
EventBus.entity_died.emit(target)
|
||||
|
||||
func _on_heal_requested(healer: Node, target: Node, amount: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
if not is_instance_valid(target) or not Stats.has(target):
|
||||
return
|
||||
var hp: float = float(Stats.get_stat(target, "health", 0.0))
|
||||
if hp <= 0.0:
|
||||
return
|
||||
var max_hp: float = float(Stats.get_stat(target, "max_health", 0.0))
|
||||
var new_hp: float = min(max_hp, hp + amount)
|
||||
Stats.set_stat(target, "health", new_hp)
|
||||
EventBus.health_changed.emit(target, new_hp, max_hp)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://h362ftxb0cns
|
||||
uid://bm2g2nxog0nxu
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
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
|
||||
var tavern_bar: ProgressBar = null
|
||||
var tavern_label: Label = null
|
||||
var wave_label: Label = null
|
||||
var level_label: Label = null
|
||||
var xp_bar: ProgressBar = null
|
||||
var xp_label: Label = 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)
|
||||
EventBus.tavern_damaged.connect(_on_tavern_damaged)
|
||||
EventBus.wave_started.connect(_on_wave_started)
|
||||
EventBus.wave_timer_tick.connect(_on_wave_timer_tick)
|
||||
EventBus.xp_gained.connect(_on_xp_gained)
|
||||
EventBus.level_up.connect(_on_level_up)
|
||||
_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)
|
||||
_init_tavern_bar(hud)
|
||||
_init_xp_bar(hud)
|
||||
|
||||
func _init_tavern_bar(hud: CanvasLayer) -> void:
|
||||
var container := VBoxContainer.new()
|
||||
container.name = "TavernContainer"
|
||||
container.anchor_left = 0.5
|
||||
container.anchor_right = 0.5
|
||||
container.offset_left = -150
|
||||
container.offset_right = 150
|
||||
container.offset_top = 10
|
||||
container.add_theme_constant_override("separation", 2)
|
||||
wave_label = Label.new()
|
||||
wave_label.text = "Welle 1 — 60:00"
|
||||
wave_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
wave_label.add_theme_font_size_override("font_size", 18)
|
||||
container.add_child(wave_label)
|
||||
var title := Label.new()
|
||||
title.text = "Taverne"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 14)
|
||||
container.add_child(title)
|
||||
tavern_bar = ProgressBar.new()
|
||||
tavern_bar.custom_minimum_size = Vector2(300, 22)
|
||||
tavern_bar.show_percentage = false
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.3, 0.1, 0.1, 1)
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(0.9, 0.7, 0.2, 1)
|
||||
tavern_bar.add_theme_stylebox_override("background", bg)
|
||||
tavern_bar.add_theme_stylebox_override("fill", fill)
|
||||
tavern_bar.max_value = 5000.0
|
||||
tavern_bar.value = 5000.0
|
||||
container.add_child(tavern_bar)
|
||||
tavern_label = Label.new()
|
||||
tavern_label.text = "5000/5000"
|
||||
tavern_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
tavern_label.anchor_left = 0.0
|
||||
tavern_label.anchor_right = 1.0
|
||||
tavern_bar.add_child(tavern_label)
|
||||
hud.add_child(container)
|
||||
|
||||
func _on_tavern_damaged(current: float, max_val: float) -> void:
|
||||
if tavern_bar:
|
||||
tavern_bar.max_value = max_val
|
||||
tavern_bar.value = current
|
||||
if tavern_label:
|
||||
tavern_label.text = "%d/%d" % [current, max_val]
|
||||
|
||||
func _init_xp_bar(hud: CanvasLayer) -> void:
|
||||
var container := VBoxContainer.new()
|
||||
container.name = "LevelContainer"
|
||||
container.anchor_left = 1.0
|
||||
container.anchor_top = 1.0
|
||||
container.anchor_right = 1.0
|
||||
container.anchor_bottom = 1.0
|
||||
container.offset_left = -260
|
||||
container.offset_top = -80
|
||||
container.offset_right = -10
|
||||
container.offset_bottom = -10
|
||||
container.add_theme_constant_override("separation", 2)
|
||||
level_label = Label.new()
|
||||
level_label.text = "Level 1"
|
||||
level_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
level_label.add_theme_font_size_override("font_size", 20)
|
||||
container.add_child(level_label)
|
||||
xp_bar = ProgressBar.new()
|
||||
xp_bar.custom_minimum_size = Vector2(250, 22)
|
||||
xp_bar.max_value = 1
|
||||
xp_bar.value = 0
|
||||
xp_bar.show_percentage = false
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.1, 0.2, 0.1, 1)
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(0.3, 0.9, 0.3, 1)
|
||||
xp_bar.add_theme_stylebox_override("background", bg)
|
||||
xp_bar.add_theme_stylebox_override("fill", fill)
|
||||
xp_label = Label.new()
|
||||
xp_label.text = "0/1"
|
||||
xp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
xp_label.anchor_left = 0.0
|
||||
xp_label.anchor_right = 1.0
|
||||
xp_bar.add_child(xp_label)
|
||||
container.add_child(xp_bar)
|
||||
hud.add_child(container)
|
||||
_update_xp_ui()
|
||||
|
||||
func _on_xp_gained(_player: Node, _amount: int) -> void:
|
||||
_update_xp_ui()
|
||||
|
||||
func _on_level_up(_player: Node, _new_level: int) -> void:
|
||||
_update_xp_ui()
|
||||
|
||||
func _update_xp_ui() -> void:
|
||||
if level_label:
|
||||
level_label.text = "Level %d" % PlayerData.level
|
||||
if xp_bar:
|
||||
xp_bar.max_value = PlayerData.xp_to_next
|
||||
xp_bar.value = PlayerData.xp
|
||||
if xp_label:
|
||||
xp_label.text = "%d/%d" % [PlayerData.xp, PlayerData.xp_to_next]
|
||||
|
||||
func _on_wave_started(_wave_number: int) -> void:
|
||||
_update_wave_label()
|
||||
|
||||
func _on_wave_timer_tick(_seconds_remaining: float) -> void:
|
||||
_update_wave_label()
|
||||
|
||||
func _update_wave_label() -> void:
|
||||
if not wave_label:
|
||||
return
|
||||
var secs: int = int(max(0.0, GameState.wave_timer_remaining))
|
||||
var mm: int = secs / 60
|
||||
var ss: int = secs % 60
|
||||
wave_label.text = "Welle %d — %02d:%02d" % [GameState.current_wave, mm, ss]
|
||||
|
||||
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 +0,0 @@
|
||||
uid://da87wrxxuhws1
|
||||
@@ -1,88 +1,41 @@
|
||||
extends Node
|
||||
|
||||
const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn")
|
||||
const BOSS_STATS: Resource = preload("res://scenes/enemy/boss_stats.tres")
|
||||
const INVASION_COUNT := 16
|
||||
const SPAWN_RADIUS := 45.0
|
||||
@onready var spawn_system: Node = get_node("../SpawnSystem")
|
||||
|
||||
var active_enemies: Array = []
|
||||
var active: bool = false
|
||||
var invasion_enemies: Array[Node] = []
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.wave_timer_tick.connect(_on_wave_timer_tick)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.tavern_destroyed.connect(_on_tavern_destroyed)
|
||||
EventBus.entity_died.connect(_on_died)
|
||||
|
||||
func _on_wave_timer_tick(seconds_remaining: float) -> void:
|
||||
if active:
|
||||
func start(wave: int) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
if seconds_remaining > 0:
|
||||
return
|
||||
var has_alive_red := false
|
||||
for p in get_tree().get_nodes_in_group("red_portal"):
|
||||
if is_instance_valid(p) and PortalData.is_alive(p):
|
||||
has_alive_red = true
|
||||
break
|
||||
if not has_alive_red:
|
||||
return
|
||||
trigger()
|
||||
|
||||
func trigger() -> void:
|
||||
active = true
|
||||
invasion_enemies.clear()
|
||||
var tavern: Node = get_tree().get_first_node_in_group("tavern")
|
||||
if not tavern:
|
||||
active = false
|
||||
return
|
||||
var world: Node = get_tree().current_scene
|
||||
var scale: float = PlayerData.level_scale * 10.0
|
||||
for enemy in get_tree().get_nodes_in_group("red_enemies"):
|
||||
if is_instance_valid(enemy):
|
||||
_convert_to_invasion(enemy, tavern)
|
||||
for i in range(INVASION_COUNT):
|
||||
active_enemies.clear()
|
||||
var count: int = 6 + wave * 2
|
||||
for i in range(count):
|
||||
var angle: float = randf() * TAU
|
||||
var pos := Vector3(cos(angle) * SPAWN_RADIUS, 0, sin(angle) * SPAWN_RADIUS)
|
||||
var enemy: Node = ENEMY_SCENE.instantiate()
|
||||
enemy.spawn_scale = scale
|
||||
world.add_child(enemy)
|
||||
enemy.global_position = pos
|
||||
_convert_to_invasion(enemy, tavern)
|
||||
var boss_angle: float = randf() * TAU
|
||||
var boss_pos := Vector3(cos(boss_angle) * SPAWN_RADIUS, 0, sin(boss_angle) * SPAWN_RADIUS)
|
||||
var boss: Node = ENEMY_SCENE.instantiate()
|
||||
boss.add_to_group("boss")
|
||||
boss.stats = BOSS_STATS
|
||||
boss.spawn_scale = scale
|
||||
world.add_child(boss)
|
||||
boss.global_position = boss_pos
|
||||
_convert_to_invasion(boss, tavern)
|
||||
EventBus.invasion_started.emit(invasion_enemies)
|
||||
var pos := Vector3(cos(angle) * 50.0, 0.5, sin(angle) * 50.0)
|
||||
var e: Node = spawn_system.spawn_enemy_at(pos, true, 1.0 + wave * 0.5)
|
||||
if e:
|
||||
active_enemies.append(e)
|
||||
e.set("origin", Vector3.ZERO)
|
||||
var v: Node = get_tree().get_first_node_in_group("village")
|
||||
if v:
|
||||
e.set("invasion_target", v)
|
||||
|
||||
func _convert_to_invasion(enemy: Node, tavern: Node) -> void:
|
||||
enemy.add_to_group("invasion")
|
||||
var data_source: Node = BossData if enemy.is_in_group("boss") else EnemyData
|
||||
data_source.set_stat(enemy, "target", tavern)
|
||||
data_source.set_stat(enemy, "state", 1)
|
||||
invasion_enemies.append(enemy)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
func _on_died(entity: Node) -> void:
|
||||
if not active:
|
||||
return
|
||||
if entity not in invasion_enemies:
|
||||
if entity in active_enemies:
|
||||
active_enemies.erase(entity)
|
||||
if active_enemies.is_empty():
|
||||
active = false
|
||||
EventBus.invasion_ended.emit(true)
|
||||
|
||||
func damage_village(amount: float) -> void:
|
||||
var v: Node = get_tree().get_first_node_in_group("village")
|
||||
if v == null:
|
||||
return
|
||||
invasion_enemies.erase(entity)
|
||||
var alive_count := 0
|
||||
for e in invasion_enemies:
|
||||
if is_instance_valid(e):
|
||||
alive_count += 1
|
||||
if alive_count == 0:
|
||||
_end_invasion(true)
|
||||
|
||||
func _end_invasion(success: bool) -> void:
|
||||
active = false
|
||||
invasion_enemies.clear()
|
||||
EventBus.invasion_ended.emit(success)
|
||||
|
||||
func _on_tavern_destroyed() -> void:
|
||||
active = false
|
||||
EventBus.game_over.emit()
|
||||
EventBus.damage_requested.emit(v, v, amount, Element.NONE)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://841gb4nrydai
|
||||
uid://ka51njikkl
|
||||
|
||||
57
systems/inventory_system.gd
Normal file
57
systems/inventory_system.gd
Normal file
@@ -0,0 +1,57 @@
|
||||
extends Node
|
||||
|
||||
var inventories: Dictionary = {}
|
||||
var equipment: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_deregistered.connect(_on_dereg)
|
||||
|
||||
func _on_dereg(entity: Node) -> void:
|
||||
inventories.erase(entity)
|
||||
equipment.erase(entity)
|
||||
|
||||
func add_item(player: Node, item_id: StringName, amount: int) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
if not player in inventories:
|
||||
inventories[player] = {}
|
||||
inventories[player][item_id] = inventories[player].get(item_id, 0) + amount
|
||||
EventBus.inventory_changed.emit(player)
|
||||
if not player.is_multiplayer_authority():
|
||||
_sync_inventory.rpc_id(player.get_multiplayer_authority(), inventories[player])
|
||||
|
||||
func remove_item(player: Node, item_id: StringName, amount: int) -> bool:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return false
|
||||
if not player in inventories:
|
||||
return false
|
||||
if inventories[player].get(item_id, 0) < amount:
|
||||
return false
|
||||
inventories[player][item_id] -= amount
|
||||
if inventories[player][item_id] <= 0:
|
||||
inventories[player].erase(item_id)
|
||||
EventBus.inventory_changed.emit(player)
|
||||
if not player.is_multiplayer_authority():
|
||||
_sync_inventory.rpc_id(player.get_multiplayer_authority(), inventories[player])
|
||||
return true
|
||||
|
||||
func get_amount(player: Node, item_id: StringName) -> int:
|
||||
if not player in inventories:
|
||||
return 0
|
||||
return inventories[player].get(item_id, 0)
|
||||
|
||||
func get_inventory(player: Node) -> Dictionary:
|
||||
return inventories.get(player, {})
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _sync_inventory(data: Dictionary) -> void:
|
||||
var local: Node = _find_local_player()
|
||||
if local:
|
||||
inventories[local] = data
|
||||
EventBus.inventory_changed.emit(local)
|
||||
|
||||
func _find_local_player() -> Node:
|
||||
for p in get_tree().get_nodes_in_group("player"):
|
||||
if p.is_multiplayer_authority():
|
||||
return p
|
||||
return null
|
||||
1
systems/inventory_system.gd.uid
Normal file
1
systems/inventory_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctqu2tiv8jw3a
|
||||
27
systems/loot_system.gd
Normal file
27
systems/loot_system.gd
Normal file
@@ -0,0 +1,27 @@
|
||||
extends Node
|
||||
|
||||
const LOOT_SCENE: PackedScene = preload("res://scenes/entities/loot/loot_drop.tscn")
|
||||
|
||||
func _loot_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Loot")
|
||||
if n == null:
|
||||
n = get_node_or_null("/root/Dungeon/EntityRoot/Loot")
|
||||
return n
|
||||
|
||||
func drop_loot_for(entity: Node, pos: Vector3) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
var root := _loot_root()
|
||||
if root == null:
|
||||
return
|
||||
var amount: int = 1
|
||||
var item_id: StringName = &"essence"
|
||||
if entity.is_in_group("boss"):
|
||||
amount = 5 + randi() % 5
|
||||
var loot: Area3D = LOOT_SCENE.instantiate()
|
||||
loot.item_id = item_id
|
||||
loot.amount = amount
|
||||
loot.name = "Loot_%d" % (Time.get_ticks_msec() + randi() % 1000)
|
||||
root.add_child(loot, true)
|
||||
loot.global_position = pos + Vector3(randf_range(-0.5, 0.5), 0.4, randf_range(-0.5, 0.5))
|
||||
EventBus.loot_dropped.emit([item_id], loot.global_position)
|
||||
1
systems/loot_system.gd.uid
Normal file
1
systems/loot_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dh3ctjeb17wc5
|
||||
26
systems/map_system.gd
Normal file
26
systems/map_system.gd
Normal file
@@ -0,0 +1,26 @@
|
||||
extends Node
|
||||
|
||||
func get_marker_data() -> Array:
|
||||
var out: Array = []
|
||||
for p in get_tree().get_nodes_in_group("player"):
|
||||
if not (p is Node3D):
|
||||
continue
|
||||
out.append({"pos": (p as Node3D).global_position, "color": Color(0.3, 0.6, 1.0), "label": Net.player_names.get(p.peer_id if p.has_method("get") else 1, "P")})
|
||||
for e in get_tree().get_nodes_in_group("enemies"):
|
||||
if e is Node3D:
|
||||
out.append({"pos": (e as Node3D).global_position, "color": Color(0.9, 0.2, 0.2), "label": ""})
|
||||
for g in get_tree().get_nodes_in_group("gates"):
|
||||
if g is Node3D:
|
||||
var c: Color = Color(0.95, 0.2, 0.15) if g.is_in_group("red_gate") else Color(0.85, 0.55, 0.2)
|
||||
out.append({"pos": (g as Node3D).global_position, "color": c, "label": "Gate"})
|
||||
for portal in get_tree().get_nodes_in_group("portals"):
|
||||
if portal is Node3D:
|
||||
var c2: Color = Color(0.95, 0.2, 0.15) if portal.is_in_group("red_portal") else Color(0.3, 0.6, 1.0)
|
||||
out.append({"pos": (portal as Node3D).global_position, "color": c2, "label": "Portal"})
|
||||
for n in get_tree().get_nodes_in_group("npc"):
|
||||
if n is Node3D:
|
||||
out.append({"pos": (n as Node3D).global_position, "color": Color(0.9, 0.85, 0.5), "label": ""})
|
||||
for v in get_tree().get_nodes_in_group("village"):
|
||||
if v is Node3D:
|
||||
out.append({"pos": (v as Node3D).global_position, "color": Color(0.4, 0.85, 0.4), "label": "Village"})
|
||||
return out
|
||||
1
systems/map_system.gd.uid
Normal file
1
systems/map_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kafmxo67e5xt
|
||||
@@ -1,237 +0,0 @@
|
||||
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)
|
||||
EventBus.invasion_started.connect(_on_invasion_started)
|
||||
_init_nameplates.call_deferred()
|
||||
|
||||
func _on_invasion_started(enemies: Array) -> void:
|
||||
for enemy in enemies:
|
||||
if is_instance_valid(enemy):
|
||||
_setup_nameplate.call_deferred(enemy)
|
||||
|
||||
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
|
||||
if not nameplate.texture:
|
||||
_setup_nameplate(entity)
|
||||
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:
|
||||
if not nameplate.texture:
|
||||
_setup_nameplate(enemy)
|
||||
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:
|
||||
if not nameplate.texture:
|
||||
_setup_nameplate(portal)
|
||||
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 +0,0 @@
|
||||
uid://yijhaxo8anul
|
||||
39
systems/npc_system.gd
Normal file
39
systems/npc_system.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
extends Node
|
||||
|
||||
const NPC_SCENE: PackedScene = preload("res://scenes/entities/npc/npc.tscn")
|
||||
|
||||
func _npc_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Npcs")
|
||||
return n
|
||||
|
||||
func spawn_default_npcs() -> void:
|
||||
var root := _npc_root()
|
||||
if root == null:
|
||||
return
|
||||
var profiles: Array = [
|
||||
_make_profile(&"barkeep", "Brena", "Barkeep at the Crooked Wheel tavern.", "Warm, gossipy, has lived here her whole life."),
|
||||
_make_profile(&"smith", "Halvor", "Village blacksmith. Lost an eye to a portal monster.", "Gruff, terse, cares deeply for the village."),
|
||||
_make_profile(&"sage", "Eyrie", "Village sage. Reads the portals, claims to remember the time before.", "Cryptic, slow-spoken, weary."),
|
||||
_make_profile(&"farmer", "Rolf", "Tends the small fields east of the village.", "Anxious, practical, wants the wars to end."),
|
||||
]
|
||||
var radius: float = 6.0
|
||||
for i in range(profiles.size()):
|
||||
var angle: float = float(i) * TAU / float(profiles.size()) + 0.3
|
||||
var pos := Vector3(cos(angle) * radius, 0.0, sin(angle) * radius)
|
||||
var npc: StaticBody3D = NPC_SCENE.instantiate()
|
||||
npc.profile = profiles[i]
|
||||
npc.profile_id = profiles[i].npc_id
|
||||
npc.name = "Npc_%s" % str(profiles[i].npc_id)
|
||||
root.add_child(npc, true)
|
||||
npc.global_position = pos
|
||||
|
||||
func _make_profile(id: StringName, name: String, lore: String, pers: String) -> NpcProfile:
|
||||
var p := NpcProfile.new()
|
||||
p.npc_id = id
|
||||
p.display_name = name
|
||||
p.lore = lore
|
||||
p.personality = pers
|
||||
p.fallback_text = "..."
|
||||
p.greeting = "Hallo, Reisender."
|
||||
p.color = Color(0.55 + randf() * 0.35, 0.5 + randf() * 0.3, 0.4 + randf() * 0.3)
|
||||
return p
|
||||
1
systems/npc_system.gd.uid
Normal file
1
systems/npc_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://pk3bpy5mo0vb
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
gate.dungeon_variant = entity.stats.variant
|
||||
EventBus.portal_defeated.emit(entity)
|
||||
entity.queue_free()
|
||||
@@ -1 +0,0 @@
|
||||
uid://c5sqw08twxtnh
|
||||
@@ -1,45 +1,54 @@
|
||||
extends Node
|
||||
|
||||
var respawn_timer := 0.0
|
||||
var is_dead := false
|
||||
var _dead: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_dead:
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
respawn_timer -= delta
|
||||
EventBus.respawn_tick.emit(respawn_timer)
|
||||
if respawn_timer <= 0:
|
||||
_respawn()
|
||||
var to_respawn: Array = []
|
||||
for entity in _dead.keys():
|
||||
if not is_instance_valid(entity):
|
||||
to_respawn.append(entity)
|
||||
continue
|
||||
_dead[entity] -= delta
|
||||
if _dead[entity] <= 0.0:
|
||||
to_respawn.append(entity)
|
||||
for e in to_respawn:
|
||||
_dead.erase(e)
|
||||
if is_instance_valid(e) and e.is_in_group("player"):
|
||||
_respawn_player(e)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity != PlayerData:
|
||||
if not is_instance_valid(entity):
|
||||
return
|
||||
if is_dead:
|
||||
return
|
||||
is_dead = true
|
||||
respawn_timer = PlayerData.respawn_time
|
||||
var player: Node = get_tree().get_first_node_in_group("player")
|
||||
if not player:
|
||||
return
|
||||
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("Ability").set_process_unhandled_input(false)
|
||||
player.get_node("Targeting").set_process_unhandled_input(false)
|
||||
if entity.is_in_group("player"):
|
||||
var t: float = float(Stats.get_stat(entity, "respawn_time", 3.0))
|
||||
_dead[entity] = t
|
||||
if entity.has_method("set_dead"):
|
||||
entity.set_dead.rpc(true)
|
||||
|
||||
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("Ability").set_process_unhandled_input(true)
|
||||
player.get_node("Targeting").set_process_unhandled_input(true)
|
||||
PlayerData.respawn()
|
||||
func _respawn_player(entity: Node) -> void:
|
||||
var max_hp: float = float(Stats.get_stat(entity, "max_health", 100.0))
|
||||
var max_shield: float = float(Stats.get_stat(entity, "max_shield", 0.0))
|
||||
Stats.set_stat(entity, "health", max_hp)
|
||||
Stats.set_stat(entity, "shield", max_shield)
|
||||
EventBus.health_changed.emit(entity, max_hp, max_hp)
|
||||
EventBus.shield_changed.emit(entity, max_shield, max_shield)
|
||||
EventBus.entity_respawned.emit(entity)
|
||||
if entity.has_method("set_dead"):
|
||||
entity.set_dead.rpc(false)
|
||||
if entity.has_method("teleport_to"):
|
||||
entity.teleport_to.rpc(_village_spawn_point())
|
||||
|
||||
func _village_spawn_point() -> Vector3:
|
||||
var v: Variant = get_tree().get_first_node_in_group("village")
|
||||
if v is Node3D:
|
||||
return (v as Node3D).global_position + Vector3(0, 1, 0)
|
||||
return Vector3(0, 1, 0)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://b1qkvoqvmd21h
|
||||
uid://84nufoafsg60
|
||||
|
||||
@@ -1,23 +1,89 @@
|
||||
extends Node
|
||||
|
||||
@export var tank_set: AbilitySet
|
||||
@export var damage_set: AbilitySet
|
||||
@export var healer_set: AbilitySet
|
||||
var ability_sets: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.role_change_requested.connect(_on_role_change_requested)
|
||||
_apply_role.call_deferred(PlayerData.current_role)
|
||||
ability_sets[GameState.ROLE_TANK] = _build_tank_set()
|
||||
ability_sets[GameState.ROLE_DAMAGE] = _build_damage_set()
|
||||
ability_sets[GameState.ROLE_HEALER] = _build_healer_set()
|
||||
EventBus.role_changed.connect(_on_role_changed)
|
||||
|
||||
func _on_role_change_requested(_player: Node, role: int) -> void:
|
||||
_apply_role(role)
|
||||
func get_set(role: int) -> AbilitySet:
|
||||
return ability_sets.get(role, ability_sets[GameState.ROLE_DAMAGE])
|
||||
|
||||
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)
|
||||
func _on_role_changed(player: Node, role: int) -> void:
|
||||
var set: AbilitySet = get_set(role)
|
||||
var passive: Ability = null
|
||||
for a in set.abilities:
|
||||
if a.type == Ability.Type.PASSIVE:
|
||||
passive = a
|
||||
break
|
||||
var effects: Node = get_node_or_null("../EffectSystem")
|
||||
if effects and passive and effects.has_method("apply_passive_aura"):
|
||||
effects.apply_passive_aura(player, passive)
|
||||
|
||||
func _make_ability(name: String, type: int, damage: float, ability_range: float, cooldown: float, opts: Dictionary = {}) -> Ability:
|
||||
var a := Ability.new()
|
||||
a.ability_name = StringName(name)
|
||||
a.type = type
|
||||
a.damage = damage
|
||||
a.ability_range = ability_range
|
||||
a.cooldown = cooldown
|
||||
a.uses_gcd = opts.get("uses_gcd", true)
|
||||
a.aoe_radius = opts.get("aoe_radius", 0.0)
|
||||
a.is_heal = opts.get("is_heal", false)
|
||||
a.shield_value = opts.get("shield_value", 0.0)
|
||||
a.shield_multiplier = opts.get("shield_multiplier", 0.0)
|
||||
a.passive_stat = opts.get("passive_stat", &"")
|
||||
a.passive_value = opts.get("passive_value", 0.5)
|
||||
a.passive_radius = opts.get("passive_radius", 50.0)
|
||||
a.element = opts.get("element", Element.NONE)
|
||||
return a
|
||||
|
||||
func _build_tank_set() -> AbilitySet:
|
||||
var s := AbilitySet.new()
|
||||
s.role_name = &"Tank"
|
||||
s.color = Color(0.3, 0.5, 0.95)
|
||||
s.aa_damage = 5.0
|
||||
s.aa_range = 3.0
|
||||
s.aa_is_heal = false
|
||||
s.abilities = [
|
||||
_make_ability("Smash", Ability.Type.SINGLE, 15.0, 3.0, 2.0, {"shield_value": 10.0}),
|
||||
_make_ability("Sweep", Ability.Type.AOE, 10.0, 3.0, 3.0, {"aoe_radius": 10.0, "shield_value": 5.0}),
|
||||
_make_ability("Bulwark", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}),
|
||||
_make_ability("Aegis", Ability.Type.ULT, 0.0, 0.0, 20.0, {"shield_multiplier": 3.0}),
|
||||
_make_ability("Fortify", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_shield", "passive_value": 0.5, "passive_radius": 50.0})
|
||||
]
|
||||
return s
|
||||
|
||||
func _build_damage_set() -> AbilitySet:
|
||||
var s := AbilitySet.new()
|
||||
s.role_name = &"Damage"
|
||||
s.color = Color(0.95, 0.3, 0.3)
|
||||
s.aa_damage = 10.0
|
||||
s.aa_range = 10.0
|
||||
s.aa_is_heal = false
|
||||
s.abilities = [
|
||||
_make_ability("Bolt", Ability.Type.SINGLE, 30.0, 20.0, 2.0, {"element": Element.FIRE}),
|
||||
_make_ability("Inferno", Ability.Type.AOE, 20.0, 20.0, 3.0, {"aoe_radius": 5.0, "element": Element.FIRE}),
|
||||
_make_ability("Barrier", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}),
|
||||
_make_ability("Rain", Ability.Type.ULT, 50.0, 20.0, 15.0, {"aoe_radius": 3.0, "element": Element.FIRE}),
|
||||
_make_ability("Power", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_damage", "passive_value": 0.5, "passive_radius": 50.0})
|
||||
]
|
||||
return s
|
||||
|
||||
func _build_healer_set() -> AbilitySet:
|
||||
var s := AbilitySet.new()
|
||||
s.role_name = &"Healer"
|
||||
s.color = Color(0.4, 0.85, 0.4)
|
||||
s.aa_damage = 1.0
|
||||
s.aa_range = 20.0
|
||||
s.aa_is_heal = true
|
||||
s.abilities = [
|
||||
_make_ability("Mend", Ability.Type.SINGLE, 15.0, 20.0, 2.0, {"is_heal": true}),
|
||||
_make_ability("Bloom", Ability.Type.AOE, 10.0, 20.0, 3.0, {"aoe_radius": 20.0, "is_heal": true}),
|
||||
_make_ability("Ward", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}),
|
||||
_make_ability("Sanctuary", Ability.Type.ULT, 25.0, 20.0, 15.0, {"aoe_radius": 3.0, "is_heal": true}),
|
||||
_make_ability("Devotion", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_heal", "passive_value": 0.5, "passive_radius": 50.0})
|
||||
]
|
||||
return s
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://cuwueo5v43kap
|
||||
uid://ci60ni5o8h0ie
|
||||
|
||||
@@ -1,74 +1,39 @@
|
||||
extends Node
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_process_player(delta)
|
||||
_process_entities(delta, EnemyData.entities)
|
||||
_process_entities(delta, BossData.entities)
|
||||
func _ready() -> void:
|
||||
if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
set_physics_process(false)
|
||||
else:
|
||||
set_physics_process(true)
|
||||
|
||||
func _process_player(delta: float) -> void:
|
||||
if not PlayerData.alive or PlayerData.max_shield <= 0:
|
||||
var _regen_accum: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
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:
|
||||
_regen_accum += delta
|
||||
if _regen_accum < 0.20:
|
||||
return
|
||||
var dt: float = _regen_accum
|
||||
_regen_accum = 0.0
|
||||
for entity in Stats.entities():
|
||||
if not is_instance_valid(entity):
|
||||
continue
|
||||
var data: Dictionary = entities[entity]
|
||||
if not data["alive"]:
|
||||
var hp: float = float(Stats.get_stat(entity, "health", 0.0))
|
||||
if hp <= 0.0:
|
||||
continue
|
||||
var max_shield: float = data["max_shield"]
|
||||
if max_shield <= 0:
|
||||
var max_shield: float = float(Stats.get_stat(entity, "max_shield", 0.0))
|
||||
if max_shield <= 0.0:
|
||||
continue
|
||||
var shield: float = float(Stats.get_stat(entity, "shield", 0.0))
|
||||
var timer: float = float(Stats.get_stat(entity, "shield_regen_timer", 0.0))
|
||||
if timer > 0.0:
|
||||
Stats.set_stat(entity, "shield_regen_timer", max(0.0, timer - dt))
|
||||
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(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
|
||||
data_source.set_stat(target, "shield_regen_timer", 0.0)
|
||||
var absorbed: float = min(amount, shield)
|
||||
shield -= absorbed
|
||||
data_source.set_shield(target, shield)
|
||||
if shield <= 0:
|
||||
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
|
||||
var regen: float = float(Stats.get_stat(entity, "shield_regen", 0.0))
|
||||
if regen <= 0.0:
|
||||
regen = max_shield * 0.10
|
||||
var new_shield: float = min(max_shield, shield + regen * dt)
|
||||
Stats.set_stat(entity, "shield", new_shield)
|
||||
EventBus.shield_changed.emit(entity, new_shield, max_shield)
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://rsnpuf77o0sn
|
||||
uid://dhgary4qpucki
|
||||
|
||||
@@ -1,51 +1,93 @@
|
||||
extends Node
|
||||
|
||||
const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn")
|
||||
const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn")
|
||||
const PORTAL_SCENE: PackedScene = preload("res://scenes/entities/portal/portal.tscn")
|
||||
const GATE_SCENE: PackedScene = preload("res://scenes/entities/gate/gate.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.health_changed.connect(_on_health_changed)
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
func _enemy_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Enemies")
|
||||
if n == null:
|
||||
n = get_node_or_null("/root/Dungeon/EntityRoot/Enemies")
|
||||
return n
|
||||
|
||||
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
|
||||
if not entity.is_in_group("portals"):
|
||||
return
|
||||
if current <= 0:
|
||||
return
|
||||
var data: Dictionary = PortalData.entities.get(entity, {})
|
||||
if data.is_empty():
|
||||
return
|
||||
var ratio: float = current / max_val
|
||||
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 _portal_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Portals")
|
||||
if n == null:
|
||||
n = get_node_or_null("/root/Dungeon/EntityRoot/Portals")
|
||||
return n
|
||||
|
||||
func _spawn_enemies(portal: Node, count: int) -> void:
|
||||
var spawned: Array = []
|
||||
var is_red: bool = portal.is_in_group("red_portal")
|
||||
var portal_bonus: float = 10.0 if is_red else 1.0
|
||||
var total_scale: float = PlayerData.level_scale * portal_bonus
|
||||
for j in range(count):
|
||||
var entity: Node = ENEMY_SCENE.instantiate()
|
||||
entity.spawn_scale = total_scale
|
||||
var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2))
|
||||
portal.get_parent().add_child(entity)
|
||||
if is_red:
|
||||
entity.add_to_group("red_enemies")
|
||||
entity.global_position = portal.global_position + offset
|
||||
EnemyData.set_stat(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 _gate_root() -> Node3D:
|
||||
var n: Node = get_node_or_null("/root/World/EntityRoot/Gates")
|
||||
if n == null:
|
||||
n = get_node_or_null("/root/Dungeon/EntityRoot/Gates")
|
||||
return n
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if entity.is_in_group("portals"):
|
||||
PortalData.deregister(entity)
|
||||
func spawn_enemy_at(pos: Vector3, strong: bool = false, mult: float = 1.0) -> Node:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return null
|
||||
var root := _enemy_root()
|
||||
if root == null:
|
||||
return null
|
||||
var e: CharacterBody3D = ENEMY_SCENE.instantiate()
|
||||
var stats := EnemyStats.new()
|
||||
var s: float = mult * (3.0 if strong else 1.0)
|
||||
stats.max_health = 30.0 * s
|
||||
stats.attack_damage = 5.0 * s
|
||||
stats.attack_range = 2.0
|
||||
stats.attack_cooldown = 1.5
|
||||
stats.speed = 3.0
|
||||
stats.aggro_radius = 12.0
|
||||
stats.xp_value = 5.0 * s
|
||||
e.stats_resource = stats
|
||||
e.name = "Enemy_%d" % (Time.get_ticks_msec() + randi() % 1000)
|
||||
root.add_child(e, true)
|
||||
e.global_position = pos
|
||||
return e
|
||||
|
||||
func spawn_boss_at(pos: Vector3, mult: float = 1.0) -> Node:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return null
|
||||
var root := _enemy_root()
|
||||
if root == null:
|
||||
return null
|
||||
var b: CharacterBody3D = ENEMY_SCENE.instantiate()
|
||||
b.is_boss = true
|
||||
var stats := BossStats.new()
|
||||
stats.max_health = 250.0 * mult
|
||||
stats.attack_damage = 12.0 * mult
|
||||
stats.attack_range = 3.0
|
||||
stats.attack_cooldown = 1.6
|
||||
stats.speed = 3.5
|
||||
stats.xp_value = 100.0 * mult
|
||||
b.stats_resource = stats
|
||||
b.name = "Boss_%d" % Time.get_ticks_msec()
|
||||
root.add_child(b, true)
|
||||
b.global_position = pos
|
||||
return b
|
||||
|
||||
func spawn_portal_at(pos: Vector3, is_red: bool) -> Node:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return null
|
||||
var root := _portal_root()
|
||||
if root == null:
|
||||
return null
|
||||
var p: StaticBody3D = PORTAL_SCENE.instantiate()
|
||||
p.is_red = is_red
|
||||
p.name = "Portal_%d" % Time.get_ticks_msec()
|
||||
root.add_child(p, true)
|
||||
p.global_position = pos
|
||||
EventBus.portal_spawned.emit(p)
|
||||
return p
|
||||
|
||||
func spawn_gate_at(pos: Vector3, is_red: bool) -> Node:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return null
|
||||
var root := _gate_root()
|
||||
if root == null:
|
||||
return null
|
||||
var g: StaticBody3D = GATE_SCENE.instantiate()
|
||||
g.is_red = is_red
|
||||
g.name = "Gate_%s_%d" % ["red" if is_red else "n", Time.get_ticks_msec() + randi() % 1000]
|
||||
root.add_child(g, true)
|
||||
g.global_position = pos
|
||||
return g
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://c84voxmnaifyt
|
||||
uid://k0yi8mwpdhgy
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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)
|
||||
if target and (target.is_in_group("enemies") or target.is_in_group("portals")):
|
||||
PlayerData.in_combat = true
|
||||
PlayerData.combat_timer = PlayerData.combat_timeout
|
||||
|
||||
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 +0,0 @@
|
||||
uid://bvjmdmof4vcyr
|
||||
@@ -1,47 +1,77 @@
|
||||
extends Node
|
||||
|
||||
const WAVE_DURATION := 60.0
|
||||
const NORMAL_GATES: int = 3
|
||||
const TIMER_SECONDS: float = 600.0
|
||||
|
||||
var tick_accumulator := 0.0
|
||||
@onready var spawn_system: Node = get_node("../SpawnSystem")
|
||||
|
||||
var timer_remaining: float = TIMER_SECONDS
|
||||
var red_gate: Node = null
|
||||
var red_portal_done: bool = false
|
||||
var active: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.portal_defeated.connect(_on_portal_defeated)
|
||||
EventBus.gate_destroyed.connect(_on_gate_destroyed)
|
||||
EventBus.boss_defeated.connect(_on_boss_defeated)
|
||||
EventBus.invasion_ended.connect(_on_invasion_ended)
|
||||
call_deferred("_start_run")
|
||||
|
||||
func _start_run() -> void:
|
||||
if not GameState.run_initialized:
|
||||
GameState.current_wave = 1
|
||||
GameState.wave_timer_remaining = WAVE_DURATION
|
||||
GameState.run_initialized = true
|
||||
EventBus.run_started.emit(GameState.current_wave)
|
||||
EventBus.wave_started.emit(GameState.current_wave)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if GameState.wave_timer_remaining <= 0:
|
||||
func start_wave(n: int) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
GameState.wave_timer_remaining -= delta
|
||||
tick_accumulator += delta
|
||||
if tick_accumulator >= 1.0:
|
||||
tick_accumulator -= 1.0
|
||||
EventBus.wave_timer_tick.emit(max(0.0, GameState.wave_timer_remaining))
|
||||
if GameState.wave_timer_remaining <= 0:
|
||||
GameState.wave_timer_remaining = 0.0
|
||||
EventBus.wave_timer_tick.emit(0.0)
|
||||
GameState.current_wave = n
|
||||
red_portal_done = false
|
||||
red_gate = null
|
||||
timer_remaining = TIMER_SECONDS
|
||||
active = true
|
||||
EventBus.wave_started.emit(n)
|
||||
_spawn_initial_gates()
|
||||
|
||||
func _on_portal_defeated(portal: Node) -> void:
|
||||
if not portal.is_in_group("red_portal"):
|
||||
func _spawn_initial_gates() -> void:
|
||||
for i in range(NORMAL_GATES):
|
||||
var pos := _random_outer_position(20.0, 40.0)
|
||||
spawn_system.spawn_gate_at(pos, false)
|
||||
red_gate = spawn_system.spawn_gate_at(_random_outer_position(30.0, 45.0), true)
|
||||
|
||||
func _random_outer_position(min_d: float, max_d: float) -> Vector3:
|
||||
var angle: float = randf() * TAU
|
||||
var d: float = randf_range(min_d, max_d)
|
||||
return Vector3(cos(angle) * d, 0.5, sin(angle) * d)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not active:
|
||||
return
|
||||
_advance_wave()
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
timer_remaining = max(0.0, timer_remaining - delta)
|
||||
EventBus.wave_timer_tick.emit(timer_remaining)
|
||||
if timer_remaining <= 0.0 and not red_portal_done:
|
||||
active = false
|
||||
EventBus.invasion_started.emit()
|
||||
var inv: Node = get_node_or_null("../InvasionSystem")
|
||||
if inv and inv.has_method("start"):
|
||||
inv.start(GameState.current_wave)
|
||||
|
||||
func _on_gate_destroyed(gate: Node) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
if not gate.is_in_group("red_gate"):
|
||||
var pos := _random_outer_position(20.0, 40.0)
|
||||
spawn_system.spawn_gate_at(pos, false)
|
||||
|
||||
func _on_boss_defeated(_boss: Node) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
if GameState.dungeon_red and not red_portal_done:
|
||||
red_portal_done = true
|
||||
active = false
|
||||
EventBus.wave_ended.emit(GameState.current_wave, true)
|
||||
var t := get_tree().create_timer(2.0)
|
||||
t.timeout.connect(func(): start_wave(GameState.current_wave + 1))
|
||||
|
||||
func _on_invasion_ended(success: bool) -> void:
|
||||
if not success:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
_advance_wave()
|
||||
|
||||
func _advance_wave() -> void:
|
||||
EventBus.wave_ended.emit(GameState.current_wave, true)
|
||||
GameState.current_wave += 1
|
||||
GameState.wave_timer_remaining = WAVE_DURATION
|
||||
tick_accumulator = 0.0
|
||||
EventBus.wave_started.emit(GameState.current_wave)
|
||||
if success:
|
||||
EventBus.wave_ended.emit(GameState.current_wave, true)
|
||||
var t := get_tree().create_timer(2.0)
|
||||
t.timeout.connect(func(): start_wave(GameState.current_wave + 1))
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://chfcocmkb0wnp
|
||||
uid://ccmpr0d2tgs45
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
extends Node
|
||||
|
||||
const XP_PER_ENEMY: int = 3
|
||||
const XP_PER_BOSS: int = 30
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.entity_died.connect(_on_entity_died)
|
||||
EventBus.boss_defeated.connect(_on_boss)
|
||||
|
||||
func _on_entity_died(entity: Node) -> void:
|
||||
if not entity or not is_instance_valid(entity):
|
||||
func _on_boss(boss: Node) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
if not entity.is_in_group("enemies"):
|
||||
var amount: float = float(Stats.get_stat(boss, "xp_value", 100.0))
|
||||
if GameState.dungeon_red:
|
||||
amount *= 5.0
|
||||
for player in get_tree().get_nodes_in_group("player"):
|
||||
award(player, amount)
|
||||
|
||||
func award_for_enemy(enemy: Node) -> void:
|
||||
if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null):
|
||||
return
|
||||
var is_boss: bool = entity.is_in_group("boss")
|
||||
var data_source: Node = BossData if is_boss else EnemyData
|
||||
var scale_val: Variant = data_source.get_stat(entity, "scale")
|
||||
var scale: float = scale_val if scale_val != null else 1.0
|
||||
var base_xp: int = XP_PER_BOSS if is_boss else XP_PER_ENEMY
|
||||
PlayerData.add_xp(int(base_xp * scale))
|
||||
var amount: float = float(Stats.get_stat(enemy, "xp_value", 5.0))
|
||||
for player in get_tree().get_nodes_in_group("player"):
|
||||
var d: float = (player as Node3D).global_position.distance_to((enemy as Node3D).global_position)
|
||||
if d <= 50.0:
|
||||
award(player, amount)
|
||||
|
||||
func award(player: Node, amount: float) -> void:
|
||||
if not Stats.has(player):
|
||||
return
|
||||
var xp: float = float(Stats.get_stat(player, "xp", 0.0)) + amount
|
||||
var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0))
|
||||
var level: int = int(Stats.get_stat(player, "level", 1))
|
||||
while xp >= to_next:
|
||||
xp -= to_next
|
||||
level += 1
|
||||
to_next = round(to_next * 1.6)
|
||||
_level_up(player, level)
|
||||
Stats.set_stat(player, "xp", xp)
|
||||
Stats.set_stat(player, "xp_to_next", to_next)
|
||||
Stats.set_stat(player, "level", level)
|
||||
EventBus.xp_gained.emit(player, amount)
|
||||
|
||||
func _level_up(player: Node, new_level: int) -> void:
|
||||
for stat in [&"max_health", &"max_shield", &"speed"]:
|
||||
var cur: float = float(Stats.get_stat(player, stat, 0.0))
|
||||
Stats.set_stat(player, stat, cur * 1.4)
|
||||
Stats.set_stat(player, "health", Stats.get_stat(player, "max_health"))
|
||||
Stats.set_stat(player, "shield", Stats.get_stat(player, "max_shield"))
|
||||
EventBus.level_up.emit(player, new_level)
|
||||
EventBus.health_changed.emit(player, Stats.get_stat(player, "health"), Stats.get_stat(player, "max_health"))
|
||||
EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), Stats.get_stat(player, "max_shield"))
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://clivdryqcvfmh
|
||||
uid://crvitg8qvkip5
|
||||
|
||||
Reference in New Issue
Block a user