lib/gamefic/active.rb



# frozen_string_literal: true


require 'set'
require 'gamefic/active/cue'
require 'gamefic/active/messaging'
require 'gamefic/active/narratives'

module Gamefic
  # The Active module gives entities the ability to perform actions and

  # participate in scenes. The Actor class, for example, is an Entity

  # subclass that includes this module.

  #

  module Active
    include Logging
    include Messaging

    # The most recently started cue.

    #

    # @return [Cue, nil]

    attr_reader :last_cue

    # The cue that will be started on the next turn.

    #

    # @return [Cue, nil]

    attr_reader :next_cue

    # The narratives in which the entity is participating.

    #

    # @return [Narratives]

    def narratives
      @narratives ||= Narratives.new
    end

    # An array of commands waiting to be executed.

    #

    # @return [Array<String>]

    def queue
      @queue ||= []
    end

    # Data that will be sent to the user. The output is typically sent after a

    # scene has started and before the user is prompted for input.

    #

    # The output object attached to the actor is always frozen. Authors should

    # use on_player_output blocks to modify output to be sent to the user.

    #

    # @return [Props::Output]

    def output
      last_cue&.output || Props::Output::EMPTY
    end

    # Perform a command.

    #

    # The command's action will be executed immediately, regardless of the

    # entity's state.

    #

    # @example Send a command as a string

    #   character.perform "take the key"

    #

    # @param input [String]

    # @return [Command, nil]

    def perform(input)
      dispatchers.push Dispatcher.new(Request.new(self, input))
      dispatchers.last.execute.tap do |command|
        dispatchers.pop
        @acting = true if command&.active?
      end
    end

    # Quietly perform a command.

    # This method executes the command exactly as #perform does, except it

    # buffers the resulting output instead of sending it to messages.

    #

    # @param input [String]

    # @return [String] The output that resulted from performing the command.

    def quietly(input)
      messenger.buffer { perform input }
    end

    # Perform an action.

    # This is functionally identical to the `perform` method, except the

    # action must be declared as a verb with a list of arguments. Use

    # `perform` if you need to parse a string as a command.

    #

    # The command will be executed immediately, regardless of the entity's

    # state.

    #

    # @example

    #   character.execute :take, @key

    #

    # @param verb [Symbol]

    # @param params [Array]

    # @return [Command, nil]

    def execute(verb, *params)
      dispatchers.push Dispatcher.new(Order.new(self, verb, params))
      dispatchers.last.execute.tap do |command|
        dispatchers.pop
        @acting = true if command&.active?
      end
    end

    # Proceed to the next Action in the current stack.

    # This method is typically used in Action blocks to cascade through

    # multiple implementations of the same verb.

    #

    # @example Proceed through two implementations of a verb

    #   introduction do |actor|

    #     actor[:has_eaten] = false # Initial value

    #   end

    #

    #   respond :eat do |actor|

    #     actor.tell "You eat something."

    #     actor[:has_eaten] = true

    #   end

    #

    #   respond :eat do |actor|

    #     # This version will be executed first because it was implemented last

    #     if actor[:has_eaten]

    #       actor.tell "You already ate."

    #     else

    #       actor.proceed # Execute the previous implementation

    #     end

    #   end

    #

    # @return [Action, nil]

    def proceed
      dispatchers.last&.proceed
    end

    # Cue a scene to start in the next turn.

    #

    # @raise [ArgumentError] if the scene is not valid

    #

    # @param scene [Class<Scene::Base>, Symbol]

    # @param context [Hash] Extra data to pass to the scene's props

    # @return [Cue]

    def cue scene, **context
      return @next_cue if @next_cue&.key == scene && @next_cue&.context == context

      logger.debug "Overwriting existing cue `#{@next_cue.key}` with `#{scene}`" if @next_cue

      @next_cue = Cue.new(self, scene, current, **context)
    end
    alias prepare cue

    # Restart the scene from the most recent cue.

    #

    # @return [Cue, nil]

    def recue
      (@next_cue = @last_cue&.restart) || warn_nil('No scene to recue')
    end

    # True if the actor is ready to leave the game.

    #

    def concluding?
      narratives.empty? || last_cue&.type == 'Conclusion'
    end

    def accessible
      []
    end

    # True if the actor is participating in any narratives.

    #

    def participating?
      !narratives.empty?
    end

    # True if the actor can perform the verb (i.e., an active narrative

    # understands it).

    #

    # @param verb [String, Symbol]

    def can?(verb)
      narratives.understand?(verb)
    end

    # Move next_cue into last_cue. This method is typically called by the

    # narrator at the start of a turn. It returns the last cue.

    #

    # @return [Cue, nil]

    def rotate_cue
      @acting = false
      @last_cue = @next_cue
      @next_cue = nil
      @last_cue
    end

    # True if the actor performed a command this turn. False if the actor has

    # not performed a command yet or has only performed meta commands.

    #

    def acting?
      @acting ||= false
    end

    # The input from the last finished cue.

    #

    # @return [String, nil]

    def last_input
      output.last_input
    end

    private

    # Get the currently bound or primary narrative.

    #

    # @return [Narrative, nil]

    def current
      Binding.for(self) || narratives.first
    end

    # @return [Array<Dispatcher>]

    def dispatchers
      @dispatchers ||= []
    end

    def warn_nil(message)
      logger.warn message
      nil
    end
  end
end