Introduction
View SourceSetup
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.
Mix dependency:
def deps do
[
{:dev_joy, "~> 2.0"}
]
end
Rebar dependency:
{deps,[
{dev_joy, "~> 2.0"}
]}.
defmodule MyApp.Scenes.Main do
use DevJoy.Scene
part :main, page_title: "Hello!" do
dialog :john_doe, "Hello world"
end
end
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
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
defmodule MyApp.PageLive do
use MyAppWeb, :live_view
# …
alias DevJoy.Scene.Dialog
# …
def render(%{item: %Dialog{}} = assigns) do
~H"""
{@item.content}
"""
end
end
By default all template files and their locale translations
are stored in i18n
directory inside project's priv directory.
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.
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))
See JavaScript documentation for more information.
Example: Importing data from external sources
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 to create the DSL for code samples
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
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
Example: Implementing the Character base module for a Discourse forum API
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
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
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