class_name EffectContainer extends Node signal effect_added(effect_id: StringName) signal effect_removed(effect_id: StringName) const EffectInstanceScript := preload("res://resources/effects/effect_instance.gd") @export var beats_per_measure := 4 @export var initial_effects: Array[Resource] = [] var effects: Array[RefCounted] = [] func _ready() -> void: var bus := _event_bus_or_null() if bus != null and bus.has_signal("beat_ticked") and not bus.is_connected("beat_ticked", _on_beat_ticked): bus.connect("beat_ticked", _on_beat_ticked) for definition: Resource in initial_effects: add_effect(definition, &"loadout") func _exit_tree() -> void: var bus := _event_bus_or_null() if bus != null and bus.has_signal("beat_ticked") and bus.is_connected("beat_ticked", _on_beat_ticked): bus.disconnect("beat_ticked", _on_beat_ticked) func _process(delta: float) -> void: tick_time(delta) func add_effect(definition: Resource, source: Variant = &"-") -> void: if definition == null: return var effect_id := StringName(str(definition.get("id"))) var max_stacks := int(definition.get("max_stacks")) for effect: RefCounted in effects: if StringName(str(effect.definition.get("id"))) == effect_id: effect.stacks = mini(effect.stacks + 1, max(1, max_stacks)) effect.remaining = float(definition.get("duration")) effect.source = source effect_added.emit(effect_id) return effects.append(EffectInstanceScript.create(definition, source)) effect_added.emit(effect_id) func set_effect_stacks(effect_id: StringName, stacks: int) -> void: if stacks <= 0: remove_effect(effect_id) return for effect: RefCounted in effects: if StringName(str(effect.definition.get("id"))) == effect_id: var max_stacks := maxi(1, int(effect.definition.get("max_stacks"))) effect.stacks = mini(stacks, max_stacks) return func effect_stacks(effect_id: StringName) -> int: for effect: RefCounted in effects: if StringName(str(effect.definition.get("id"))) == effect_id: return int(effect.stacks) return 0 func remove_effect(effect_id: StringName) -> void: for index: int in range(effects.size() - 1, -1, -1): var effect: RefCounted = effects[index] if StringName(str(effect.definition.get("id"))) == effect_id: effects.remove_at(index) effect_removed.emit(effect_id) func tick_time(delta: float) -> void: for effect: RefCounted in effects: effect.tick_time(delta) _prune_expired() func tick_beats(beats: float) -> void: for effect: RefCounted in effects: effect.tick_beats(beats) _prune_expired() func active_count() -> int: return effects.size() func active_effect_ids() -> Array[StringName]: var ids: Array[StringName] = [] for effect: RefCounted in effects: ids.append(StringName(str(effect.definition.get("id")))) return ids func active_effect_summaries() -> Array[Dictionary]: var summaries: Array[Dictionary] = [] for effect: RefCounted in effects: summaries.append({ "id": StringName(str(effect.definition.get("id"))), "duration_type": StringName(str(effect.definition.get("duration_type"))), "remaining": float(effect.remaining), "stacks": int(effect.stacks), "source": _source_label(effect.source), }) return summaries func dispatch_event(event_name: StringName, context: Dictionary = {}) -> void: var triggered: Array[Resource] = [] for effect: RefCounted in effects: if StringName(str(effect.definition.get("trigger_event"))) != event_name: continue if effect.has_method("matches_event_context") and not bool(effect.call("matches_event_context", context)): continue var next_effects = effect.definition.get("trigger_effects") if not next_effects is Array: continue for definition: Resource in next_effects: if definition != null: triggered.append(definition) for effect: RefCounted in effects: if effect.has_method("tick_event"): effect.call("tick_event", event_name, context) _prune_expired() for definition: Resource in triggered: add_effect(definition) func damage_mult(action: Resource = null) -> float: return stat_multiplier(&"damage_mult", action) func cost_mult(action: Resource = null) -> float: return stat_multiplier(&"cost_mult", action) func move_mult(action: Resource = null) -> float: return stat_multiplier(&"move_mult", action) func defense_modifiers() -> Array[Resource]: return _active_modifiers("defense_modifiers") func action_rule_modifiers() -> Array[Resource]: return _active_modifiers("action_rule_modifiers") func stat_multiplier(stat: StringName, _action: Resource = null) -> float: var multiplier := 1.0 for effect: RefCounted in effects: for modifier: Resource in _stat_modifiers(effect): if StringName(str(modifier.get("stat"))) != stat: continue if str(modifier.get("operation")) == "add": multiplier += float(modifier.get("value")) * maxi(1, int(effect.stacks)) else: multiplier *= float(modifier.get("value")) return multiplier func _stat_modifiers(effect: RefCounted) -> Array: var modifiers = effect.definition.get("stat_modifiers") return modifiers if modifiers is Array else [] func _active_modifiers(property_name: String) -> Array[Resource]: var result: Array[Resource] = [] for effect: RefCounted in effects: var modifiers = effect.definition.get(property_name) if not modifiers is Array: continue for modifier: Resource in modifiers: if modifier != null: result.append(modifier) return result func _source_label(source: Variant) -> StringName: if source == null: return &"-" if source is StringName: return source if source is String: return StringName(source) if source is Node: return StringName((source as Node).name) return StringName(str(source)) func _prune_expired() -> void: for index: int in range(effects.size() - 1, -1, -1): var effect: RefCounted = effects[index] if effect.is_expired(): var effect_id := StringName(str(effect.definition.get("id"))) effects.remove_at(index) effect_removed.emit(effect_id) func _on_beat_ticked(beat_index: int) -> void: tick_beats(1.0) if beats_per_measure > 0 and beat_index % beats_per_measure == 0: dispatch_event(&"on_measure_start", {"beat_index": beat_index}) dispatch_event(&"on_beat", {"beat_index": beat_index}) func _event_bus_or_null() -> Node: if not is_inside_tree(): return null return get_tree().root.get_node_or_null("EventBus")