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.
Dependency
Mix dependency:
def deps do
[
{:dev_joy, "~> 2.0"}
]
endRebar dependency:
{deps,[
{dev_joy, "~> 2.0"}
]}.DSL
defmodule MyApp.Scenes.Main do
use DevJoy.Scene
part :main, page_title: "Hello!" do
dialog :john_doe, "Hello world"
end
endGetting data
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
endLiveView
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
endRender
defmodule MyApp.PageLive do
use MyAppWeb, :live_view
# …
alias DevJoy.Scene.Dialog
# …
def render(%{item: %Dialog{}} = assigns) do
~H"""
{@item.content}
"""
end
endI18n
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))JS
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
Struct
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
endDSL
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
endExample: Implementing the Character base module for a Discourse forum API
Forum module
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
endCache module
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))
endForum module with ForumCache support
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