# frozen_string_literal: true module FelECS module Components @component_map = [] class << self # Creates a new {FelECS::ComponentManager component manager}. # # @example # # Here color is set to default to red # # while max and current are nil until set. # # When you make a new component using this component manager # # these are the values and accessors it will have. # FelECS::Component.new('Health', :max, :current, color: 'red') # # @param component_name [String] Name of your new component manager. Must be stylized in the format of constants in Ruby # @param attrs [:Symbols] New components made with this manager will include these symbols as accessors, the values of these accessors will default to nil # @param attrs_with_defaults [Keyword: DefaultValue] New components made with this manager will include these keywords as accessors, their defaults set to the values given to the keywords # @return [ComponentManager] def new(component_name, *attrs, **attrs_with_defaults) if FelECS::Components.const_defined?(component_name) raise(NameError.new, "Component Manager '#{component_name}' is already defined") end const_set(component_name, Class.new(FelECS::ComponentManager) {}) update_const_cache attrs.each do |attr| if FelECS::Components.const_get(component_name).method_defined?(attr.to_s) || FelECS::Components.const_get(component_name).method_defined?("#{attr}=") raise NameError, "The attribute name \"#{attr}\" is already a method" end FelECS::Components.const_get(component_name).attr_accessor attr end attrs_with_defaults.each do |attr, _default| attrs_with_defaults[attr] = _default.dup FelECS::Components.const_get(component_name).attr_reader attr FelECS::Components.const_get(component_name).define_method("#{attr}=") do |value| attr_changed_trigger_systems(attr) unless value.equal? send(attr) instance_variable_set("@#{attr}", value) end end FelECS::Components.const_get(component_name).define_method(:set_defaults) do attrs_with_defaults.each do |attr, default| instance_variable_set("@#{attr}", default.dup) end end FelECS::Components.const_get(component_name) end # Stores the components managers in {FelECS::Components}. This # is needed because calling `FelECS::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 # @!visibility private def const_cache @const_cache || update_const_cache end # Updates the array that stores the constants. # Used internally by FelECS # @!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 component 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 end # Component Managers are what is used to create individual components which can be attached to entities. # When a Component is created from a Component Manager that has accessors given to it, you can set or get the values of those accessors using standard ruby message sending (e.g +@component.var = 5+), or by using the {#to_h} and {#update_attrs} methods instead. class ComponentManager # 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 references to systems that should be triggered when a # component from this manager is added. # Do not edit this array as it is managed by FelECS automatically. # @return [Array] def addition_triggers @addition_triggers ||= [] end # Stores references to systems that should be triggered when a # component from this manager is removed. # Do not edit this array as it is managed by FelECS 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 FelECS automatically. # @return [Hash>] def attr_triggers @attr_triggers ||= {} end # Creates a new component and sets the values of the attributes given to it. If an attritbute is not passed then it will remain as the default. # @param attrs [Keyword: Value] You can pass any number of Keyword-Value pairs # @return [Component] def initialize(**attrs) # Prepare the object # (this is a function created with metaprogramming # in FelECS::Components) set_defaults # Fill params attrs.each do |key, value| send "#{key}=", value end # Save Component self.class.push self end class << self # Makes component managers behave like arrays with additional # methods for managing the array # @!visibility private def respond_to_missing?(method, *) if _data.respond_to? method true else super end end # Makes component managers behave like arrays with additional # methods for managing the array # @!visibility private def method_missing(method, *args, **kwargs, &block) if _data.respond_to? method _data.send(method, *args, **kwargs, &block) else super end end # 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 references to systems that should be triggered when this # component is added to an enitity. # Do not edit this array as it is managed by FelECS automatically. # @return [Array] def addition_triggers @addition_triggers ||= [] end # Stores references to systems that should be triggered when this # component is removed from an enitity. # Do not edit this array as it is managed by FelECS automatically. # @return [Array] def removal_triggers @removal_triggers ||= [] end # Stores references to systems that should be triggered when an # attribute from this component changed. # Do not edit this hash as it is managed by FelECS automatically. # @return [Hash] def attr_triggers @attr_triggers ||= {} end # @return [Array] Array of all Components that belong to a given component manager # @!visibility private def _data @data ||= [] end end # Entities that have this component # @return [Array] def entities @entities ||= [] end # A single entity. Use this if you expect the component to only belong to one entity and you want to access it. # @return [Component] def entity if entities.empty? Warning.warn("This component belongs to NO entities but you called the method that is intended for components belonging to a single entity.\nYou may have a bug in your logic.") elsif entities.length > 1 Warning.warn("This component belongs to MANY entities but you called the method that is intended for components belonging to a single entity.\nYou may have a bug in your logic.") end entities.first end # Update attribute values using a hash or keywords. # @return [Hash] Hash of updated attributes def update_attrs(**opts) opts.each do |key, value| send "#{key}=", value end end # Execute systems that have been added to execute on variable change # @return [Boolean] +true+ # @!visibility private def attr_changed_trigger_systems(attr) systems_to_execute = self.class.attr_triggers[attr] systems_to_execute = [] if systems_to_execute.nil? systems_to_execute |= attr_triggers[attr] unless attr_triggers[attr].nil? systems_to_execute.sort_by(&:priority).reverse_each(&:call) true end # Removes this component from the list and purges all references to this Component from other Entities, as well as its data. # @return [Boolean] +true+. def delete addition_triggers.each do |system| system.clear_triggers component_or_manager: self end entities.reverse_each do |entity| entity.remove self end self.class._data.delete self instance_variables.each do |var| instance_variable_set(var, nil) end true end # @return [Hash] A hash, where all the keys are attributes storing their respective values. def to_h return_hash = instance_variables.each_with_object({}) do |key, final| final[key.to_s.delete_prefix('@').to_sym] = instance_variable_get(key) end return_hash.delete(:attr_triggers) return_hash end # Export all data into a JSON String, which could then later be loaded or saved to a file # TODO: This function is not yet complete # @return [String] a JSON formatted String # def to_json # # should return a json or hash of all data in this component # end end end