lib/kalevala/world/room.ex

defmodule Kalevala.World.Room.Private do
  @moduledoc """
  Store private information for a room, e.g. characters in the room
  """

  defstruct characters: [], item_instances: []
end

defmodule Kalevala.World.Room.Feature do
  @moduledoc """
  A room feature is a highlighted part of a room
  """

  defstruct [:id, :keyword, :short_description, :description]
end

defmodule Kalevala.World.Room do
  @moduledoc """
  Rooms are the base unit of space in Kalevala
  """

  use GenServer

  require Logger

  alias Kalevala.Event
  alias Kalevala.Event.Message
  alias Kalevala.World.Room.Callbacks
  alias Kalevala.World.Room.Context
  alias Kalevala.World.Room.Events
  alias Kalevala.World.Room.Handler
  alias Kalevala.World.Room.Private

  @doc """
  Confirm movement for a character
  """
  def confirm_movement(event = %Event{topic: Voting, data: %{aborted: true}}, _room_id) do
    event
  end

  def confirm_movement(event, room_id) do
    GenServer.call(global_name(room_id), event)
  end

  @doc """
  Replace internal room state
  """
  def update(pid, room) do
    GenServer.call(pid, {:update, room})
  end

  @doc """
  Replace internal room items state
  """
  def update_items(pid, item_instances) do
    GenServer.call(pid, {:update_items, item_instances})
  end

  @doc false
  def global_name(%{id: id}), do: global_name(id)

  def global_name(room_id), do: {:global, {__MODULE__, room_id}}

  @doc false
  def start_link(options) do
    genserver_options = options.genserver_options
    options = Map.delete(options, :genserver_options)

    GenServer.start_link(__MODULE__, options, genserver_options)
  end

  @impl true
  def init(options) do
    Logger.info("Room starting - #{options.room.id}")

    config = options.config
    room = Callbacks.init(options.room)

    state = %{
      data: room,
      supervisor_name: config.supervisor_name,
      private: %Private{
        item_instances: options.item_instances
      }
    }

    {:ok, state, {:continue, :initialized}}
  end

  @impl true
  def handle_continue(:initialized, state) do
    Callbacks.initialized(state.data)
    {:noreply, state}
  end

  @impl true
  def handle_call(event = %Event{topic: Event.Movement.Voting}, _from, state) do
    {context, event} = Handler.confirm_movement(state, event)

    Context.handle_context(context)

    state = Map.put(state, :data, context.data)

    {:reply, event, state}
  end

  def handle_call({:update, room}, _from, state) do
    state = %{state | data: room}
    {:reply, :ok, state}
  end

  def handle_call({:update_items, item_instances}, _from, state) do
    state = %{state | private: %{state.private | item_instances: item_instances}}
    {:reply, :ok, state}
  end

  @impl true
  def handle_info(event = %Event{}, state) do
    Events.handle_event(event, state)
  end

  def handle_info(message = %Message{}, state) do
    context =
      state
      |> Handler.event(message)
      |> Context.handle_context()

    state = Map.put(state, :data, context.data)

    {:noreply, state}
  end
end

defmodule Kalevala.World.Room.Handler do
  @moduledoc false

  alias Kalevala.World.Room.Callbacks
  alias Kalevala.World.Room.Context

  def event(state, event) do
    Callbacks.event(state.data, Context.new(state), event)
  end

  # Items

  def load_item(state, item_instance) do
    Callbacks.load_item(state.data, item_instance)
  end

  def item_request_drop(state, event, item_instance) do
    Callbacks.item_request_drop(state.data, Context.new(state), event, item_instance)
  end

  def item_request_pickup(state, event, item_instance) do
    Callbacks.item_request_pickup(state.data, Context.new(state), event, item_instance)
  end

  # Movement

  def exits(state), do: Callbacks.exits(state.data)

  def movement_request(state, event, room_exit) do
    Callbacks.movement_request(state.data, Context.new(state), event, room_exit)
  end

  def confirm_movement(state, event) do
    Callbacks.confirm_movement(state.data, Context.new(state), event)
  end
end

defprotocol Kalevala.World.Room.Callbacks do
  @doc """
  Called when the room is initializing
  """
  def init(room)

  @doc """
  Called after the room process is started

  Directly after `init` is completed.
  """
  def initialized(room)

  @doc """
  Callback for when a new event is received
  """
  def event(room, context, event)

  @doc """
  Load the exits for a given room

  Used when a character is trying to move, the appropriate exit is chosen
  and forwarded into movement request callbacks. Since this is a common thing
  that will happen 99% of the time, Kalevala handles it.
  """
  def exits(room)

  @doc """
  Convert item instances into items
  """
  def load_item(room, item_instance)

  @doc """
  Callback for allowing an item drop off

  A character is requesting to pick up an item, this let's the room
  accept or reject the request.
  """
  def item_request_drop(room, context, item_request_drop, item_instance)

  @doc """
  Callback for allowing an item pick up

  A character is requesting to pick up an item, this let's the room
  accept or reject the request.
  """
  def item_request_pickup(room, context, item_request_pickup, item_instance)

  @doc """
  Callback for the room to hook into movement between exits

  The character is requesting to move via an exit, a tuple allowing or rejecting
  the movement before being pitched up to the Zone should be returned.

  Can immediately terminate a room before being checked in a more detailed fashion
  with `confirm_movement/2` below.
  """
  def movement_request(room, context, movement_request, room_exit)

  @doc """
  Callback for confirming or aborting character movement

  Called while the Zone is checking each side of the exit to know if the movement
  is indeed allowed. Returning the original event allows movement to proceed, otherwise
  return an aborted event to prevent movement.

  Hook to allow for the room to reject movement for custom reasons, e.g. an NPC
  is blocking the exit and needs to be convinced first, or there is a trap blocking
  the exit.
  """
  def confirm_movement(room, context, event)
end

defmodule Kalevala.World.BasicRoom do
  @moduledoc """
  A basic room

  These are the minimum fields a room should have. You likely want more, so
  we have a protocol `Kalevala.World.Room.Callbacks` to let you create your own
  local struct.

  The following functions provide default implementations you can use for the
  `defimpl` of that protocol.

  ```elixir
  defimpl Kalevala.World.Room.Callbacks do
    alias Kalevala.World.BasicRoom

    @impl true
    def movement_request(_room, context, event, room_exit),
      do: BasicRoom.movement_request(context, event, room_exit)

    @impl true
    def confirm_movement(_room, context, event),
      do: BasicRoom.confirm_movement(context, event)

    @impl true
    def item_request_drop(_room, context, event, item_instance),
      do: BasicRoom.item_request_drop(context, event, item_instance)

    @impl true
    def item_request_pickup(_room, context, event, item_instance),
      do: BasicRoom.item_request_pickup(context, event, item_instance)

    # ...
  end
  ```
  """

  defstruct [:id]

  def movement_request(_context, event, nil), do: {:abort, event, :no_exit}

  def movement_request(_context, event, room_exit), do: {:proceed, event, room_exit}

  def confirm_movement(context, event), do: {context, event}

  def item_request_drop(_context, event, item_instance),
    do: {:proceed, event, item_instance}

  def item_request_pickup(_context, event, nil), do: {:abort, event, :no_item, nil}

  def item_request_pickup(_context, event, item_instance),
    do: {:proceed, event, item_instance}
end