README.md

<section style="align-items: center; display: grid; grid-gap: 1em; grid-template-columns: 1fr 3fr; margin-top: 1.83em;">
  <img src="assets/logo.svg" style="margin-right: 2em;">
  <div>
    <h1 style="margin-top: 0;">DevJoy</h1>
    <p>Easier creation of the Visual Novel-like games or presentations.</p>
    <div style="display: flex; gap: 0.5em; flex-wrap: wrap;">
      <a href="https://hex.pm/packages/dev_joy">
        <img src="https://img.shields.io/hexpm/v/dev_joy?label=&color=midnightblue&logo=data:image/png;base64,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&style=for-the-badge" />
      </a>
      <a href="https://gitlab.com/ramurix-software/dev-joy">
        <img src="https://img.shields.io/badge/source-%23333333.svg?logo=gitlab&style=for-the-badge" />
      </a>
      <a href="https://hexdocs.pm/dev_joy">
        <img src="https://img.shields.io/badge/📖 docs-darkgreen?style=for-the-badge" />
      </a>
      <a href="https://www.apache.org/licenses/LICENSE-2.0">
        <img src="https://img.shields.io/badge/Apache 2.0-darkmagenta?logo=apache&style=for-the-badge" />
      </a>
      <a href="https://elixir-lang.org">
        <img src="https://img.shields.io/badge/elixir-%234B275F.svg?logo=elixir&style=for-the-badge" />
      </a>
      <a href="https://phoenixframework.org">
        <img src="https://img.shields.io/badge/phoenix-%23f05423.svg?logo=phoenixframework&logoColor=white&style=for-the-badge" />
      </a>
    </div>
  </div>
</section>

## Setup

Except drawing you only have to provide a content to the game and fetch it. To "draw" your game all you need to do is to convert structs into `hologram` or `phoenix` templates or `scenic` graphs and style them respectively.

<!-- tabs-open -->
### Dependency
Mix dependency:
```elixir
def deps do
  [
    {:dev_joy, "~> 2.0"}
  ]
end
```

Rebar dependency:
```erlang
{deps,[
  {dev_joy, "~> 2.0"}
]}.
```

### DSL
```elixir
defmodule MyApp.Scenes.Main do
  use DevJoy.Scene

  part :main, page_title: "Hello!" do
    dialog :john_doe, "Hello world"
  end
end
```

### Getting data
```elixir
defmodule MyApp.PageLive do
  use MyAppWeb, :live_view

  alias MyApp.Scenes.Main

  def handle_event("btn-click", _params, socket) do
    Main.get_part_item(:main, 1)
    {:noreply, socket}
  end
end
```

### LiveView
```elixir
defmodule MyApp.PageLive do
  use MyAppWeb, :live_view
  use DevJoy.Session.LiveView
  use DevJoy.Session.Notifier

  alias DevJoy.Session
  alias MyApp.Scenes.Main

  # …

  def handle_event("btn-click", _params, socket) do
    Session.get({Main, :main, 1})
    {:noreply, socket}
  end

  def handle_info({:dev_joy, :item_changed, item}, socket) do
    {:noreply, assign(socket, item: item)}
  end
end
```

### Render
```elixir
defmodule MyApp.PageLive do
  use MyAppWeb, :live_view

  # …

  alias DevJoy.Scene.Dialog

  # …

  def render(%{item: %Dialog{}} = assigns) do
    ~H"""
    {@item.content}
    """
  end
end
```

### I18n
By default all template files and their locale translations
are stored in `i18n` directory inside project's priv directory.
```bash
PROJECT_PATH="/path/to/project/"
I18N_DIR="i18n"
SCENE_MODULE="MyApp.Scenes.Main"
mkdir -p "$PROJECT_PATH/priv/$I18N_DIR/fr/LC_MESSAGES"
touch "$PROJECT_PATH/priv/$I18N_DIR/fr/LC_MESSAGES/$SCENE_MODULE.po"
xdg-open "$PROJECT_PATH/priv/$I18N_DIR/fr/LC_MESSAGES/$SCENE_MODULE.po"
```

If translation is not found or is empty the msgid is used instead.
```elixir
scene = Main
default_locale = "en"
# get locale from cookie, db, headers, params or session

available_locales = scene.get_locales()
verified_locale = if locale in available_locales, do: locale, else: default_locale
DevJoy.API.Storage.set_locale(verified_locale)
assign(conn_or_socket, available_locales: available_locales, locale: locale)

# …
push_navigate(socket, to: some_path_with_locale(socket.assigns.locale))
```

### JS
See [JavaScript documentation](js/index.html) for more information.

<!-- tabs-close -->

## Example: Importing data from external sources
```elixir
Mix.install [:dev_joy, :nimble_csv]

priv =
  __DIR__
  |> Path.absname()
  |> Path.join("script_priv")
  |> tap(&File.mkdir_p!/1)

defmodule MainScene do
  use DevJoy.Scene, priv: priv

  alias NimbleCSV.RFC4180, as: CSV

  data = CSV.parse_string("""
  string id,dialog content
  john_doe,Hello world!
  """)

  part :main do
    for [name, content] <- data do
      # Note: prefer String.to_existing_atom/1 instead
      dialog String.to_atom(name), content
    end
  end
end

dialog = MainScene.get_part_item(:main, 1)
# => %DevJoy.Scene.Dialog{…}

dialog.character.id
# => :john_doe

dialog.content
# => "Hello world!"
```

## Example: Using [Item API](https://hexdocs.pm/dev_joy/DevJoy.API.Item.html) to create the DSL for code samples
<!-- tabs-open -->
### Struct
```elixir
defmodule MyApp.CodeSample do
  @moduledoc """
  The code sample is visually represented as a snipped containing the highlighted text

  > ### Usage {: .info}
  > 
  > The [`code sample`](`t:t/0`) always requires its [`content`](`t:content/0`) and [`language`](`t:lang/0`).
  > You can also optionally specify additional [`data`](`t:data/0`).
  > 
  >     defmodule MyApp.Scenes.Main do
  >       use DevJoy.Scene
  >
  >       import MyApp.DSL
  > 
  >       part :main do
  >         code :elixir, ~S[IO.puts("Hello world!")]
  > 
  >         code :elixir, [some: :data], ~S[IO.puts("Hello world!")]
  >     
  >         code :elixir, [some: :data] do
  >           ~S[IO.puts("Hello world!")]
  >         end
  >       end
  >     end
  """

  @enforce_keys ~w[content lang]a

  @doc """
  The [`code sample`](`t:t/0`) structure contains the following keys:

  - [`content`](`t:content/0`) - a content of the code sample
  - [`data`](`t:data/0`) - a keyword list of additional code sample data
  - [`lang`](`t:lang/0`) - a programming language of the code sample
  """
  defstruct [:content, :lang, data: []]

  @typedoc """
  The type representing the [`code sample`](`__struct__/0`) structure.
  """
  @typedoc section: :main
  @type t :: %__MODULE__{
          content: content(),
          data: data(),
          lang: lang()
        }

  @typedoc """
  The type representing the content of the code sample.
  """
  @typedoc section: :field
  @type content :: String.t()

  @typedoc """
  The type representing the additional data of the code sample.
  """
  @typedoc section: :field
  @type data :: Keyword.t()

  @typedoc """
  The type representing the lang of the code sample.
  """
  @typedoc section: :field
  @type lang :: atom
end
```

### DSL
```elixir
defmodule MyApp.DSL do
  alias MyApp.CodeSample

  @doc """
  A callback implementations generator for the `MyApp.CodeSample` struct.

  > ### Usage {: .info}
  >     defmodule MyApp.Scenes.Main do
  >       use DevJoy.Scene
  >
  >       import MyApp.DSL
  > 
  >       part :main do
  >         code :elixir, ~S[IO.puts("Hello world!")]
  > 
  >         code :elixir, [some: :data], ~S[IO.puts("Hello world!")]
  >     
  >         code :elixir, [some: :data] do
  >           ~S[IO.puts("Hello world!")]
  >         end
  >       end
  >     end
  """
  defmacro code(lang, data \\ [], content)

  @spec code(
    Scene.macro_input(CodeSample.lang()),
    Scene.macro_input(CodeSample.data()),
    do: Scene.macro_input(CodeSample.content())
  ) :: Macro.output()
  defmacro code(lang, data, do: block) do
    fields = [content: block, data: data, lang: lang]
    Item.generate(CodeSample, fields, __CALLER__, [:content])
  end

  @spec code(
    Scene.macro_input(CodeSample.lang()),
    Scene.macro_input(CodeSample.data()),
    Scene.macro_input(CodeSample.content())
  ) :: Macro.output()
  defmacro code(lang, data, content) do
    fields = [content: content, data: data, lang: lang]
    Item.generate(CodeSample, fields, __CALLER__, [:content])
  end
end
```
<!-- tabs-close -->

## Example: Implementing the [Character](https://hexdocs.pm/dev_joy/DevJoy.Character.html) base module for a Discourse forum API
<!-- tabs-open -->
### Forum module
```elixir
defmodule MyApp.Forum do
  @moduledoc "Fetches forum data using username and Discourse API."

  @behaviour DevJoy.Character

  alias DevJoy.API.Item
  alias DevJoy.Character

  @avatar_size "48"

  @impl DevJoy.Character
  def get_character_fields(id) do
    {:ok, _app_list} = Application.ensure_all_started(:req)
    fetch_forum_data(id)
  end

  @spec fetch_forum_data(Character.id()) :: Item.fields()
  defp fetch_forum_data(id) do
    :dev_joy
    |> Application.get_env(__MODULE__, endpoint: "https://elixirforum.com")
    |> Keyword.fetch!(:endpoint)
    |> then(fn endpoint ->
      endpoint
      |> Path.join("u/#{id}.json")
      |> Req.get!()
      |> then(& &1.body["user"])
      |> then(&[avatar: avatar_from_template(&1["avatar_template"], endpoint), full_name: &1["name"]])
    end)
  end

  @spec avatar_from_template(String.t(), String.t()) :: Character.portrait()
  defp avatar_from_template(template, endpoint) do
    template
    |> String.replace("{size}", @avatar_size)
    |> then(&Path.join(endpoint, &1))
  end
end
```

### Cache module
```elixir
defmodule MyApp.Forum.Cache do
  @moduledoc "Caches character base fields using `Agent`."

  alias DevJoy.API.Item
  alias DevJoy.Character

  @spec start_link :: Agent.on_start()
  def start_link, do: Agent.start_link(fn -> %{} end, name: __MODULE__)

  @spec get(Character.id()) :: Item.fields() | nil
  def get(id), do: Agent.get(__MODULE__, & &1[id])

  @spec put(Character.id(), Item.fields()) :: :ok
  def put(id, data), do: Agent.update(&Map.put(&1, id, data))
end
```

### Forum module with ForumCache support
```elixir
defmodule MyApp.Forum do
  @moduledoc "Fetches forum data using username and Discourse API."

  @behaviour DevJoy.Character

  alias DevJoy.API.Item
  alias DevJoy.Character
  alias MyApp.Forum.Cache

  @avatar_size "48"

  @impl DevJoy.Character
  def get_character_fields(id) do
    # The cache has to be started at compile-time when defined in same app
    # and this function is called by scene DSL at compile-time
    Process.whereis(Cache) || Cache.start_link()

    id
    |> Cache.get()
    |> then(&(&1 || fetch_forum_data(id)))
  end

  @spec fetch_forum_data(Character.id()) :: Item.fields()
  defp fetch_forum_data(id) do
    {:ok, _app_list} = Application.ensure_all_started(:req)

    :dev_joy
    |> Application.get_env(__MODULE__, endpoint: "https://elixirforum.com")
    |> Keyword.fetch!(:endpoint)
    |> then(fn endpoint ->
      endpoint
      |> Path.join("u/#{id}.json")
      |> Req.get!()
      |> then(& &1.body["user"])
      |> then(&[avatar: avatar_from_template(&1["avatar_template"], endpoint), full_name: &1["name"]])
    end)
    |> tap(&Cache.put(id, &1))
  end

  @spec avatar_from_template(String.t(), String.t()) :: Character.avatar()
  defp avatar_from_template(template, endpoint) do
    template
    |> String.replace("{size}", @avatar_size)
    |> then(&Path.join(endpoint, &1))
  end
end
```
<!-- tabs-close -->