Character Creation

We have a basic server, and basic user/session management. Now players need the ability to create characters.
The Character Model
We can use an Ecto generator to build us a database model and migration for the Character entity. From the web app apps/elixir_mud_web
run:
mix phx.gen.context Characters Character characters\
name:string\
player_id:references:players\
race:enum:human:elf:halfling\
level:integer
This command generates us a table migration, a new Characters
context, and a Character
model. Lets modify the migration slightly. We don't want characters to be created without level, name, race, or player. We will prevent null values on name, race, and player_id. For level we can set a default of 1.
defmodule ElixirMud.Repo.Migrations.CreateCharacters do
use Ecto.Migration
def change do
create table(:characters) do
add :name, :string, null: false
add :race, :string, null: false
add :level, :integer, null: false, default: 1
add :player_id, references(:players, on_delete: :delete_all), null: false
timestamps()
end
create index(:characters, [:player_id])
end
end
apps/elixir_mud/priv/repo/xxxx_create_characters.exs
Now we can modify the database model. We will add player_id
to cast so it can be passed in when creating a character and to validate_required
so it must be present. We can also remove level
from validate_required
since it can be omitted and will default to 1.
defmodule ElixirMud.Characters.Character do
use Ecto.Schema
import Ecto.Changeset
schema "characters" do
field :name, :string
field :level, :integer
field :race, Ecto.Enum, values: [:human, :elf, :halfling]
field :player_id, :id
timestamps()
end
@doc false
def changeset(character, attrs) do
character
|> cast(attrs, [:name, :race, :level, :player_id])
|> validate_required([:name, :race, :player_id])
end
end
apps/elixir_mud/lib/elixir_mud/characters/character.ex
Form the root of the project migrate the database with:
mix ecto.migrate
Characters Context
The generator gave us some basic management function in characters.ex
, but we will need more. Instead of creating all our query logic in the context file we can create reusable query fragments that can be composed into full queries. We will put these in the character.ex
model.
First add a few imports and aliases to the top of the module. Aliasing the current module simply gives us the ability to reference it directly instead of using __MODULE__
, which looks a bit cleaner.
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, warn: false
alias ElixirMud.Accounts.Player
alias ElixirMud.Characters.Character
Then, we will add four new query fragment functions:
- query_for_player/2 will allow us to query characters for a given player.
- query_for_id/2 will allow us to query characters by a character id.
- order_name/1 will allow us to order the characters by their name.
- query_with_player/1 will allow us to preload the player details when looking up a character.
def query_for_player(query \\ Character, %Player{} = player) do
from c in query,
where: c.player_id == ^player.id
end
def query_for_id(query \\ Character, id) do
from c in query,
where: c.id == ^id
end
def order_name(query \\ Character) do
from c in query,
order_by: [asc: c.name]
end
def query_with_player(query \\ Character) do
from c in query,
join: p in assoc(c, :player),
preload: [player: p]
end
apps/elixir_mud/lib/elixir_mud/characters/character.ex
Each of these functions take a query as their first parameter and the return a query. This allows them to be chained together to easily make complex queries.
We can modify the generated context functions for characters to use our new composable queries. list_characters/0
should order by name:
def list_characters do
Character.order_name()
|> Repo.all()
end
apps/elixir_mud/lib/elixir_mud/characters.ex
We should also be able to list characters by player:
def list_characters_for_player(%Player{} = player) do
Character.query_for_player(player)
|> Repo.all()
end
apps/elixir_mud/lib/elixir_mud/characters.ex
We can add a non dangerous get_character function using composable queries:
def get_character(id) do
Character.query_for_id(id)
|> Repo.one()
end
apps/elixir_mud/lib/elixir_mud/characters.ex
Lets make a delete function that is restricted by player:
Add a alias for Player
, near the top of the module:
alias ElixirMud.Accounts.Player
apps/elixir_mud/lib/elixir_mud/characters.ex
Add a new delete_character_for_player/2
function near delete_character/1
:
def delete_character_for_player(
%Player{} = player,
%Character{} = character
) do
Character.query_for_id(character.id)
|> Character.query_for_player(player)
|> Repo.delete_all()
end
apps/elixir_mud/lib/elixir_mud/characters.ex
Character Context Tests
We need to add unit tests for our new context functions. There is already a test file that was generated that contains tests for the generated functions. We will add a few new test cases.
test "list_characters_ or_player/1 returns characters for a specific player" do
character_fixture()
player = ElixirMud.AccountsFixtures.player_fixture()
character = character_fixture(player_id: player.id)
assert Characters.list_characters_for_player(player) == [character]
end
test "delete_character_for_player/2" do
player = ElixirMud.AccountsFixtures.player_fixture()
character = character_fixture(player_id: player.id)
Characters.delete_character_for_player(player, character)
assert Characters.list_characters() == []
end
apps/elixir_mud/test/elixir_mud/characters_test.exs
We also need to modify ElixirMud.CharactersFixtures
to automatically fill out player_id
if its not specified:
defmodule ElixirMud.CharactersFixtures do
import ElixirMud.AccountsFixtures
@moduledoc """
This module defines test helpers for creating
entities via the `ElixirMud.Characters` context.
"""
@doc """
Generate a character.
"""
def character_fixture(attrs \\ %{}) do
{:ok, character} =
attrs
|> Enum.into(%{
level: 42,
name: "some name",
race: :human,
player_id: player_fixture().id
})
|> ElixirMud.Characters.create_character()
character
end
end
apps/elixir_mud/test/support/fixtures/characters_fixture.ex
And we can run mix test
and see all greens!
Character Creation UI
Now that we have some plumbing, we can setup a UI for users to create a character. We will implement this using a Phoenix LiveView. Create a new file at apps/elixir_mud_web/lib/elixir_mud_web/live/characters/create_character_live.ex
.
Normally, it is a good idea to create a separate form component. This can then be used for creation and modification. There isn't going to be a direct need to edit characters once they are created, we will create the form handling directly in the LivePage.
A LivePage normally has the following function:
- mount/3 - Called when the page is initially mounted.
- handle_params/3 - Called when the page loads or the params change.
- render/1 - Called to render the page's content.
Here is the b CreateCharacterLive.ex
module:
defmodule ElixirMudWeb.Characters.CreateCharacterLive do
use ElixirMudWeb, :live_view
alias ElixirMud.Characters
def mount(_params, _session, socket) do
character_form =
Characters.change_character(%Characters.Character{}, %{})
|> to_form()
{:ok, assign(socket, form: character_form)}
end
def render(assigns) do
~H"""
<div class="border rounded-lg py-4 px-8">
<.header>Create a New Character</.header>
<p class="text-sm text-zinc-500">
Name your character and login to get started.
</p>
<.simple_form for={@form} phx-submit="create-character" phx-change="validate-character">
<.input field={@form[:name]} />
<:actions>
<.button>Create</.button>
</:actions>
</.simple_form>
</div>
"""
end
end
apps/elixir_mud_web/lib/elixir_mud_web/live/characters/create_character.ex
Lets break down what's happening:
This LiveView Page features a simple form allowing the player to input and submit a character name. It has two functions implemented.
The first, mount/3
, initializes the character form data and assigns it to the LiveView (socket). The Characters.change_character/2
function was generated for us by the Phoenix generator. This function accepts a character struct and a map of updates, and returns an Ecto Changeset.
The second function, render/1
renders our form with some minimal styling from Tailwind. The simple_form
component, created by phx.new
, is located in apps/elixir_mud_web/lib/elixir_mud_web/components/core_components.ex
. The phx-submit
and phx-change
attributes specify events that trigger on form submit and change respectively. We will handle those functions with handle_event/3
further down.
Routing
In order to be able to visit the page, routing must be implemented for the Live View. This route will point to /play
. Create a new live_session
block inside router.ex
. We do not want to use the other blocks, as the only other authenticated block is the player settings and we are using the blank layout for it.
scope "/", ElixirMudWeb do
pipe_through [:browser, :require_authenticated_player]
live_session :authenticated_player,
on_mount: [{ElixirMudWeb.PlayerAuth, :ensure_authenticated}] do
live "/play", Characters.CreateCharacterLive, :create
end
end
apps/elixir_mud_web/lib/elixir_mud_web/router.ex
This introduces a new scope for the page root along with a new live_session, arbitrarily named authenticated_player. Next, we define pipelines the scope will use. The :browser
pipeline is defined at the top of the module, establishing handling we would expect for browser based sessions. We also are using :require_authenticated_player
, which points to a function located in the auth.ex
file generated by the phx.gen.auth
mix task. It will redirect a visitor to the login page when they are not authenticated.
Within the scope block is the live_session
block, with its passed name, and middleware invoked during the initial session creation. This is the module and name for the middleware. In this case, its referring to the function on_mount(:ensure_authenticated, _, _, _)
; which is also inside auth.ex
.
The last piece of the puzzle is the live route, within the live_session
block. This designates the /play
route for LiveView, with a live action of :create
. The live action can be used to conditionally render based on the URL.