extends Node var active_effects: Dictionary = {} func _ready() -> void: EventBus.role_changed.connect(_on_role_changed) EventBus.entity_died.connect(_on_entity_died) EventBus.effect_requested.connect(_on_effect_requested) const AURA_REFRESH := 0.5 func _process(delta: float) -> void: for entity in active_effects.keys(): if not is_instance_valid(entity): active_effects.erase(entity) continue var entries: Array = active_effects[entity] var i: int = entries.size() - 1 while i >= 0: var entry: Dictionary = entries[i] var effect: Effect = entry["effect"] if effect.duration > 0: entry["remaining"] -= delta if entry["remaining"] <= 0: var is_aura_buff: bool = entry.get("is_aura_buff", false) entries.remove_at(i) if not is_aura_buff: EventBus.effect_expired.emit(entity, effect) _recalc_stat_buffs(entity) i -= 1 continue if effect.tick_interval > 0: entry["tick_timer"] -= delta if entry["tick_timer"] <= 0: entry["tick_timer"] += effect.tick_interval _apply_tick(entity, entry) if effect.type == Effect.Type.AURA and effect.aura_radius > 0 and effect.duration < 0: _propagate_aura(entity, entry, effect) i -= 1 func _propagate_aura(source_entity: Node, _entry: Dictionary, aura: Effect) -> void: if not source_entity is Node3D: return var players := get_tree().get_nodes_in_group("player") for player in players: if not is_instance_valid(player) or not Stats.is_alive(player): continue var dist: float = source_entity.global_position.distance_to(player.global_position) if dist > aura.aura_radius: continue if _has_aura_buff(player, aura.effect_name, source_entity): _refresh_aura_buff(player, aura.effect_name, source_entity) else: var buff := Effect.new() buff.effect_name = aura.effect_name buff.type = Effect.Type.BUFF buff.stat = aura.stat buff.value = aura.value buff.duration = AURA_REFRESH buff.is_multiplier = aura.is_multiplier _apply_aura_buff(player, buff, source_entity) func _has_aura_buff(target: Node, aura_name: String, source: Node) -> bool: if not active_effects.has(target): return false for entry in active_effects[target]: if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source: return true return false func _refresh_aura_buff(target: Node, aura_name: String, source: Node) -> void: if not active_effects.has(target): return for entry in active_effects[target]: if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source: entry["remaining"] = AURA_REFRESH return func _apply_aura_buff(target: Node, effect: Effect, source: Node) -> void: if not active_effects.has(target): active_effects[target] = [] var entry := { "effect": effect, "source": source, "remaining": effect.duration, "tick_timer": effect.tick_interval, "aura_source": source, "is_aura_buff": true, } active_effects[target].append(entry) if effect.is_multiplier: _recalc_stat_buffs(target) func apply_effect(target: Node, effect: Effect, source: Node) -> void: if not active_effects.has(target): active_effects[target] = [] var replaced := false var entries: Array = active_effects[target] for i in range(entries.size()): if entries[i]["effect"].effect_name == effect.effect_name: entries[i]["effect"] = effect entries[i]["source"] = source entries[i]["remaining"] = effect.duration entries[i]["tick_timer"] = effect.tick_interval replaced = true break if not replaced: entries.append({ "effect": effect, "source": source, "remaining": effect.duration, "tick_timer": effect.tick_interval, }) EventBus.effect_applied.emit(target, effect) if effect.is_multiplier: _recalc_stat_buffs(target) func clear_effects(entity: Node) -> void: active_effects.erase(entity) if is_instance_valid(entity): _recalc_stat_buffs(entity) func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void: apply_effect(target, effect, source) func _on_entity_died(entity: Node) -> void: clear_effects(entity) func _on_role_changed(player: Node, _role_type: int) -> void: _remove_permanent_effects(player) var role: Node = player.get_node_or_null("Role") if not role: return var ability_set: AbilitySet = role.get_ability_set() if not ability_set: return for ability in ability_set.abilities: if ability and ability.type == Ability.Type.PASSIVE: var effect := Effect.new() effect.effect_name = ability.ability_name effect.type = Effect.Type.AURA effect.stat = ability.passive_stat effect.value = ability.damage / 100.0 effect.duration = -1.0 effect.is_multiplier = true effect.aura_radius = ability.ability_range apply_effect(player, effect, player) func _remove_permanent_effects(entity: Node) -> void: if not active_effects.has(entity): return var entries: Array = active_effects[entity] var i: int = entries.size() - 1 while i >= 0: if entries[i]["effect"].duration < 0: EventBus.effect_expired.emit(entity, entries[i]["effect"]) entries.remove_at(i) i -= 1 _recalc_stat_buffs(entity) func _recalc_stat_buffs(entity: Node) -> void: var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 } if active_effects.has(entity): for entry in active_effects[entity]: var effect: Effect = entry["effect"] if effect.is_multiplier and effect.stat in mults: mults[effect.stat] += effect.value for stat in mults: Stats.set_stat(entity, "buff_" + stat, mults[stat]) EventBus.buff_changed.emit(entity, stat, mults[stat]) var base: BaseStats = Stats.get_base(entity) if base: var shield_mult: float = mults["shield"] var new_max: float = base.max_shield * shield_mult Stats.set_stat(entity, "max_shield", new_max) var shield: float = Stats.get_stat(entity, "shield") shield = min(shield, new_max) Stats.set_stat(entity, "shield", shield) EventBus.shield_changed.emit(entity, shield, new_max) func _apply_tick(entity: Node, entry: Dictionary) -> void: var effect: Effect = entry["effect"] var source: Node = entry["source"] if not is_instance_valid(source): source = entity if not effect.is_multiplier: if effect.type == Effect.Type.DEBUFF: EventBus.damage_requested.emit(source, entity, effect.value) elif effect.type == Effect.Type.BUFF: EventBus.heal_requested.emit(source, entity, effect.value)