lib/gamefic/response.rb



# frozen_string_literal: true


require 'gamefic/scriptable'

module Gamefic
  # A proc to be executed in response to a command that matches its verb and

  # queries.

  #

  class Response
    include Scriptable::Queries

    # @return [Symbol]

    attr_reader :verb

    # @return [Array<Query::Base, Query::Text>]

    attr_reader :queries

    # @return [Proc]

    attr_reader :block

    # @param verb [Symbol]

    # @param queries [Array<Object>]

    # @param meta [Boolean]

    def initialize verb, *queries, meta: false, &block
      @verb = verb&.to_sym
      @meta = meta
      @block = block
      @queries = map_queries(queries)
    end

    # The `meta?` flag is just a way for authors to identify responses that

    # serve a purpose other than performing in-game actions. Out-of-game

    # responses can include features like displaying help documentation or

    # listing credits.

    #

    def meta?
      @meta
    end

    def syntax
      @syntax ||= generate_default_syntax
    end

    # True if the Response can be executed for the given actor and command.

    #

    # @param actor [Active]

    # @param command [Command]

    def accept?(actor, command)
      command.verb == verb &&
        command.arguments.length == queries.length &&
        queries.zip(command.arguments).all? { |query, argument| query.accept?(actor, argument) }
    end

    def execute *args
      Gamefic.logger.warn "Executing unbound response #{inspect}" unless bound?
      gamefic_binding.call(*args)
    end

    # The total precision of all the response's queries.

    #

    # @note Precision is decreased if the response has a nil verb.

    #

    # @return [Integer]

    def precision
      @precision ||= calculate_precision
    end

    def inspect
      "#<#{self.class} #{([verb] + queries).map(&:inspect).join(', ')}>"
    end

    def bound?
      !!gamefic_binding.narrative
    end

    def bind(narrative)
      clone.inject_binding narrative
    end

    protected

    def inject_binding(narrative)
      @queries = map_queries(narrative.unproxy(@queries))
      @gamefic_binding = Binding.new(narrative, @block)
      self
    end

    private

    def gamefic_binding
      @gamefic_binding ||= Binding.new(nil, @block)
    end

    def generate_default_syntax
      args = queries.length.times.map { |num| num.zero? ? ':var' : ":var#{num + 1}" }
      tmpl = "#{verb} #{args.join(' ')}".strip
      Syntax.new(tmpl, tmpl)
    end

    # @return [Integer]

    def calculate_precision
      total = queries.sum(&:precision)
      total -= 1000 unless verb
      total
    end

    def map_queries(args)
      args.map { |arg| select_query(arg) }
    end

    def select_query(arg)
      case arg
      when Entity, Class, Module, Proc, Proxy::Base
        available(arg)
      when String, Regexp
        plaintext(arg)
      when Query::Base
        arg
      else
        raise ArgumentError, "invalid argument in response: #{arg.inspect}"
      end
    end
  end
end