# frozen_string_literal: true module FelFlame class Systems # How early this System should be executed in a list of Systems attr_accessor :priority # The Constant name assigned to this System # Allows overwriting the storage of triggers, such as for clearing. # This method should generally only need to be used internally and # not by a game developer. # @!visibility private attr_writer :addition_triggers, :removal_triggers, :attr_triggers # Stores all the scenes this system is a part of. attr_writer :scenes def scenes @scenes ||= [] end def priority=(priority) @priority = priority scenes.each do |scene| scene.systems = scene.systems.sort_by(&:priority) end end # Stores references to components or their managers that trigger # this component when a component or component from that manager # is added to an entity. # Do not edit this hash as it is managed by FelFlame automatically. # @return [Array] def addition_triggers @addition_triggers ||= [] end # Stores references to components or their managers that trigger # this component when a component or component from that manager # is removed from an entity. # Do not edit this hash as it is managed by FelFlame automatically. # @return [Array] def removal_triggers @removal_triggers ||= [] end # Stores references to systems that should be triggered when an # attribute from this manager is changed # Do not edit this hash as it is managed by FelFlame automatically. # @return [Hash>] def attr_triggers @attr_triggers ||= {} end class << self # Stores the systems in {FelFlame::Components}. This # is needed because calling `FelFlame::Components.constants` # will not let you iterate over the value of the constants # but will instead give you an array of symbols. This caches # the convertion of those symbols to the actual value of the # constants def const_cache @const_cache || update_const_cache end # Updates the array that stores the constants. # Used internally by FelFlame # @!visibility private def update_const_cache @const_cache = constants.map do |constant| const_get constant end end # Forwards undefined methods to the array of constants # if the array can handle the request. Otherwise tells # the programmer their code errored # @!visibility private def respond_to_missing?(method, *) if const_cache.respond_to? method true else super end end # Makes system module behave like arrays with additional # methods for managing the array # @!visibility private def method_missing(method, *args, **kwargs, &block) if const_cache.respond_to? method const_cache.send(method, *args, **kwargs, &block) else super end end end # Creates a new System which can be accessed as a constant under the namespace {FelFlame::Systems}. # The name given is what constant the system is assigned to # # @example # FelFlame::Systems.new('PassiveHeal', priority: -2) do # FelFlame::Components::Health.each do |component| # component.hp += 5 # end # end # # Give it a low priority so other systems such as a # # Poison system would kill the player first # # @param name [String] The name this system will use. Needs to to be in the Ruby Constant format. # @param priority [Integer] Which priority order this system should be executed in relative to other systems. Higher means executed earlier. # @param block [Proc] The code you wish to be executed when the system is triggered. Can be defined by using a +do end+ block or using +{ }+ braces. def initialize(name, priority: 0, &block) FelFlame::Systems.const_set(name, self) FelFlame::Systems.update_const_cache @priority = priority @block = block @scenes = [] end # Manually execute the system a single time def call @block.call end # Redefine what code is executed by this System when it is called upon. # @param block [Proc] The code you wish to be executed when the system is triggered. Can be defined by using a +do end+ block or using +{ }+ braces. def redefine(&block) @block = block end # Removes triggers from this system. This function is fairly flexible so it can accept a few different inputs # For addition and removal triggers, you can optionally pass in a component, or a manager to clear specifically # the relevant triggers for that one component or manager. If you do not pass a component or manager then it will # clear triggers for all components and managers. # For attr_triggers # @example # # To clear all triggers that execute this system when a component is added: # FelFlame::Systems::ExampleSystem.clear_triggers :addition_triggers # # Same as above but for when a component is removed instead # FelFlame::Systems::ExampleSystem.clear_triggers :removal_triggers # # Same as above but for when a component has a certain attribute changed # FelFlame::Systems::ExampleSystem.clear_triggers :attr_triggers # # # Clear a trigger from a specific component # FelFlame::Systems::ExampleSystem.clear_triggers :addition_triggers, FelFlame::Component::ExampleComponent[0] # # Clear a trigger from a specific component manager # FelFlame::Systems::ExampleSystem.clear_triggers :addition_triggers, FelFlame::Component::ExampleComponent # # # Clear the trigger that executes a system when the ':example_attr' is changes # FelFlame::Systems::ExampleSystem.clear_triggers :attr_triggers, :example_attr # @param trigger_types [:Symbols] One or more of the following trigger types: +:addition_triggers+, +:removal_triggers+, or +:attr_triggers+. If attr_triggers is used then you may pass attributes you wish to be cleared as symbols in this parameter as well # @param component_or_manager [Component or ComponentManager] The object to clear triggers from. Use Nil to clear triggers from all components associated with this system. # @return [Boolean] +true+ def clear_triggers(*trigger_types, component_or_manager: nil) trigger_types = %i[addition_triggers removal_triggers attr_triggers] if trigger_types.empty? if trigger_types.include? :attr_triggers if (trigger_types - %i[addition_triggers removal_triggers attr_triggers]).empty? if component_or_manager.nil? # remove all attrs attr_triggers.each do |cmp_or_mgr, attrs| attrs.each do |attr| next if cmp_or_mgr.attr_triggers[attr].nil? cmp_or_mgr.attr_triggers[attr].delete self end self.attr_triggers = {} end else # remove attrs relevant to comp_or_man unless attr_triggers[component_or_manager].nil? attr_triggers[component_or_manager].each do |attr| component_or_manager.attr_triggers[attr].delete self end attr_triggers[component_or_manager] = [] end end elsif component_or_manager.nil? (trigger_types - %i[addition_triggers removal_triggers attr_triggers]).each do |attr| # remove attr attr_triggers.each do |cmp_or_mgr, _attrs| cmp_or_mgr.attr_triggers[attr].delete self end end attr_triggers.delete(trigger_types - %i[addition_triggers removal_triggers attr_triggers]) else # remove attr from component_or_manager (trigger_types - %i[addition_triggers removal_triggers attr_triggers]).each do |attr| next if component_or_manager.attr_triggers[attr].nil? component_or_manager.attr_triggers[attr].delete self end attr_triggers[component_or_manager] -= trigger_types unless attr_triggers[component_or_manager].nil? end end (trigger_types & %i[removal_triggers addition_triggers] - [:attr_triggers]).each do |trigger_type| if component_or_manager.nil? # remove all removal triggers send(trigger_type).each do |trigger| trigger.send(trigger_type).delete self end send("#{trigger_type}=", []) else # remove removal trigger relevant to comp/man send(trigger_type).delete component_or_manager component_or_manager.send(trigger_type).delete self end end true end # Add a component or component manager so that it triggers this system when the component or a component from the component manager is added to an entity # @param component_or_manager [Component or ComponentManager] The component or component manager to trigger this system when added # @return [Boolean] +true+ def trigger_when_added(component_or_manager) self.addition_triggers |= [component_or_manager] component_or_manager.addition_triggers |= [self] true end # Add a component or component manager so that it triggers this system when the component or a component from the component manager is removed from an entity # @param component_or_manager [Component or ComponentManager] The component or component manager to trigger this system when removed # @return [Boolean] +true+ def trigger_when_removed(component_or_manager) self.removal_triggers |= [component_or_manager] component_or_manager.removal_triggers |= [self] true end # Add a component or component manager so that it triggers this system when a component's attribute is changed. # @return [Boolean] +true+ def trigger_when_is_changed(component_or_manager, attr) if component_or_manager.attr_triggers[attr].nil? component_or_manager.attr_triggers[attr] = [self] else component_or_manager.attr_triggers[attr] |= [self] end if attr_triggers[component_or_manager].nil? attr_triggers[component_or_manager] = [attr] else attr_triggers[component_or_manager] |= [attr] end true end end end