Start repo, claude & kimi still vibing tho
This commit is contained in:
484
lib/odinsea/database/context.ex
Normal file
484
lib/odinsea/database/context.ex
Normal file
@@ -0,0 +1,484 @@
|
||||
defmodule Odinsea.Database.Context do
|
||||
@moduledoc """
|
||||
Database context module for Odinsea.
|
||||
Provides high-level database operations for accounts, characters, and related entities.
|
||||
|
||||
Ported from Java UnifiedDB.java and MapleClient.java login functionality.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Odinsea.Repo
|
||||
alias Odinsea.Database.Schema.{Account, Character}
|
||||
alias Odinsea.Net.Cipher.LoginCrypto
|
||||
|
||||
# ==================================================================================================
|
||||
# Account Operations
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Authenticates a user with username and password.
|
||||
|
||||
Returns:
|
||||
- {:ok, account_info} on successful authentication
|
||||
- {:error, reason} on failure (reason can be :invalid_credentials, :banned, :already_logged_in, etc.)
|
||||
|
||||
Ported from MapleClient.java login() method
|
||||
"""
|
||||
def authenticate_user(username, password, ip_address \\ "") do
|
||||
# Filter username (sanitization)
|
||||
username = sanitize_username(username)
|
||||
|
||||
case Repo.get_by(Account, name: username) do
|
||||
nil ->
|
||||
Logger.warning("Login attempt for non-existent account: #{username}")
|
||||
{:error, :account_not_found}
|
||||
|
||||
account ->
|
||||
check_account_login(account, password, ip_address)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates all accounts to logged out state.
|
||||
Used during server startup/shutdown.
|
||||
"""
|
||||
def set_all_accounts_logged_off do
|
||||
Repo.update_all(Account, set: [loggedin: 0])
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates account login state.
|
||||
|
||||
States:
|
||||
- 0 = LOGIN_NOTLOGGEDIN
|
||||
- 1 = LOGIN_SERVER_TRANSITION (migrating between servers)
|
||||
- 2 = LOGIN_LOGGEDIN
|
||||
- 3 = CHANGE_CHANNEL
|
||||
"""
|
||||
def update_login_state(account_id, state, session_ip \\ nil) do
|
||||
updates = [loggedin: state]
|
||||
updates = if session_ip, do: Keyword.put(updates, :session_ip, session_ip), else: updates
|
||||
|
||||
Repo.update_all(
|
||||
from(a in Account, where: a.id == ^account_id),
|
||||
set: updates
|
||||
)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the current login state for an account.
|
||||
"""
|
||||
def get_login_state(account_id) do
|
||||
case Repo.get(Account, account_id) do
|
||||
nil -> 0
|
||||
account -> account.loggedin
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates account last login time.
|
||||
"""
|
||||
def update_last_login(account_id) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
Repo.update_all(
|
||||
from(a in Account, where: a.id == ^account_id),
|
||||
set: [lastlogin: now]
|
||||
)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Records an IP log entry for audit purposes.
|
||||
"""
|
||||
def log_ip_address(account_id, ip_address) do
|
||||
timestamp = format_timestamp(NaiveDateTime.utc_now())
|
||||
|
||||
# Using raw SQL since iplog may not have an Ecto schema yet
|
||||
sql = "INSERT INTO iplog (accid, ip, time) VALUES (?, ?, ?)"
|
||||
|
||||
case Ecto.Adapters.SQL.query(Repo, sql, [account_id, ip_address, timestamp]) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, err} ->
|
||||
Logger.error("Failed to log IP: #{inspect(err)}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if an account is banned.
|
||||
Returns {:ok, account} if not banned, {:error, :banned} if banned.
|
||||
"""
|
||||
def check_ban_status(account_id) do
|
||||
case Repo.get(Account, account_id) do
|
||||
nil -> {:error, :not_found}
|
||||
account ->
|
||||
if account.banned > 0 do
|
||||
{:error, :banned}
|
||||
else
|
||||
{:ok, account}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets temporary ban information if account is temp banned.
|
||||
"""
|
||||
def get_temp_ban_info(account_id) do
|
||||
case Repo.get(Account, account_id) do
|
||||
nil -> nil
|
||||
account ->
|
||||
if account.tempban && NaiveDateTime.compare(account.tempban, NaiveDateTime.utc_now()) == :gt do
|
||||
%{reason: account.banreason, expires: account.tempban}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Character Operations
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Loads character entries for an account in a specific world.
|
||||
Returns a list of character summaries (id, name, gm level).
|
||||
|
||||
Ported from UnifiedDB.loadCharactersEntry()
|
||||
"""
|
||||
def load_character_entries(account_id, world_id) do
|
||||
Character
|
||||
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
||||
|> select([c], %{id: c.id, name: c.name, gm: c.gm})
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the count of characters for an account in a world.
|
||||
"""
|
||||
def character_count(account_id, world_id) do
|
||||
Character
|
||||
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
||||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all character IDs for an account in a world.
|
||||
"""
|
||||
def load_character_ids(account_id, world_id) do
|
||||
Character
|
||||
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
||||
|> select([c], c.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all character names for an account in a world.
|
||||
"""
|
||||
def load_character_names(account_id, world_id) do
|
||||
Character
|
||||
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
||||
|> select([c], c.name)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads full character data by ID.
|
||||
Returns the Character struct or nil if not found.
|
||||
|
||||
TODO: Expand to load related data (inventory, skills, quests, etc.)
|
||||
"""
|
||||
def load_character(character_id) do
|
||||
Repo.get(Character, character_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a character name is already in use.
|
||||
"""
|
||||
def character_name_exists?(name) do
|
||||
Character
|
||||
|> where([c], c.name == ^name)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets character ID by name and world.
|
||||
"""
|
||||
def get_character_id(name, world_id) do
|
||||
Character
|
||||
|> where([c], c.name == ^name and c.world == ^world_id)
|
||||
|> select([c], c.id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new character.
|
||||
|
||||
TODO: Add initial items, quests, and stats based on job type
|
||||
"""
|
||||
def create_character(attrs) do
|
||||
%Character{}
|
||||
|> Character.creation_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a character (soft delete - renames and moves to deleted world).
|
||||
|
||||
Returns {:ok, character} on success, {:error, reason} on failure.
|
||||
|
||||
Ported from UnifiedDB.deleteCharacter()
|
||||
"""
|
||||
def delete_character(character_id) do
|
||||
# TODO: Check guild rank (can't delete if guild leader)
|
||||
# TODO: Remove from family
|
||||
# TODO: Handle sidekick
|
||||
|
||||
deleted_world = -1 # WORLD_DELETED
|
||||
|
||||
# Soft delete: rename with # prefix and move to deleted world
|
||||
# Need to get the character name first to construct the new name
|
||||
case Repo.get(Character, character_id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
character ->
|
||||
new_name = "#" <> character.name
|
||||
|
||||
Repo.update_all(
|
||||
from(c in Character, where: c.id == ^character_id),
|
||||
set: [
|
||||
name: new_name,
|
||||
world: deleted_world
|
||||
]
|
||||
)
|
||||
|
||||
# Clean up related records
|
||||
cleanup_character_assets(character_id)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates character stats.
|
||||
"""
|
||||
def update_character_stats(character_id, attrs) do
|
||||
case Repo.get(Character, character_id) do
|
||||
nil -> {:error, :not_found}
|
||||
character ->
|
||||
character
|
||||
|> Character.stat_changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates character position (map, spawn point).
|
||||
"""
|
||||
def update_character_position(character_id, map_id, spawn_point) do
|
||||
case Repo.get(Character, character_id) do
|
||||
nil -> {:error, :not_found}
|
||||
character ->
|
||||
character
|
||||
|> Character.position_changeset(%{map: map_id, spawnpoint: spawn_point})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Character Creation Helpers
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Gets default stats for a new character based on job type.
|
||||
|
||||
Job types:
|
||||
- 0 = Resistance
|
||||
- 1 = Adventurer
|
||||
- 2 = Cygnus
|
||||
- 3 = Aran
|
||||
- 4 = Evan
|
||||
"""
|
||||
def get_default_stats_for_job(job_type, subcategory \\ 0) do
|
||||
base_stats = %{
|
||||
level: 1,
|
||||
exp: 0,
|
||||
hp: 50,
|
||||
mp: 5,
|
||||
maxhp: 50,
|
||||
maxmp: 5,
|
||||
str: 12,
|
||||
dex: 5,
|
||||
luk: 4,
|
||||
int: 4,
|
||||
ap: 0
|
||||
}
|
||||
|
||||
case job_type do
|
||||
0 -> # Resistance
|
||||
%{base_stats | job: 3000, str: 12, dex: 5, int: 4, luk: 4}
|
||||
1 -> # Adventurer
|
||||
if subcategory == 1 do # Dual Blade
|
||||
%{base_stats | job: 430, str: 4, dex: 25, int: 4, luk: 4}
|
||||
else
|
||||
%{base_stats | job: 0, str: 12, dex: 5, int: 4, luk: 4}
|
||||
end
|
||||
2 -> # Cygnus
|
||||
%{base_stats | job: 1000, str: 12, dex: 5, int: 4, luk: 4}
|
||||
3 -> # Aran
|
||||
%{base_stats | job: 2000, str: 12, dex: 5, int: 4, luk: 4}
|
||||
4 -> # Evan
|
||||
%{base_stats | job: 2001, str: 4, dex: 4, int: 12, luk: 5}
|
||||
_ ->
|
||||
base_stats
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the default map ID for a job type.
|
||||
"""
|
||||
def get_default_map_for_job(job_type) do
|
||||
case job_type do
|
||||
0 -> 931000000 # Resistance tutorial
|
||||
1 -> 0 # Adventurer - Maple Island (handled specially)
|
||||
2 -> 130030000 # Cygnus tutorial
|
||||
3 -> 914000000 # Aran tutorial
|
||||
4 -> 900010000 # Evan tutorial
|
||||
_ -> 100000000 # Default to Henesys
|
||||
end
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Forbidden Names
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Checks if a character name is forbidden.
|
||||
"""
|
||||
def forbidden_name?(name) do
|
||||
forbidden = [
|
||||
"admin", "gm", "gamemaster", "moderator", "mod",
|
||||
"owner", "developer", "dev", "support", "help",
|
||||
"system", "server", "odinsea", "maplestory", "nexon"
|
||||
]
|
||||
|
||||
name_lower = String.downcase(name)
|
||||
Enum.any?(forbidden, fn forbidden ->
|
||||
String.contains?(name_lower, forbidden)
|
||||
end)
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Private Functions
|
||||
# ==================================================================================================
|
||||
|
||||
defp check_account_login(account, password, ip_address) do
|
||||
# Check if banned
|
||||
if account.banned > 0 && account.gm == 0 do
|
||||
Logger.warning("Banned account attempted login: #{account.name}")
|
||||
{:error, :banned}
|
||||
else
|
||||
# Check login state
|
||||
login_state = account.loggedin
|
||||
|
||||
if login_state > 0 do
|
||||
# Already logged in - could check if stale session
|
||||
Logger.warning("Account already logged in: #{account.name}")
|
||||
{:error, :already_logged_in}
|
||||
else
|
||||
verify_password(account, password, ip_address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_password(account, password, ip_address) do
|
||||
# Check various password formats
|
||||
valid =
|
||||
check_salted_sha512(account.password, password, account.salt) ||
|
||||
check_plain_match(account.password, password) ||
|
||||
check_admin_bypass(password, ip_address)
|
||||
|
||||
if valid do
|
||||
# Log successful login
|
||||
log_ip_address(account.id, ip_address)
|
||||
update_last_login(account.id)
|
||||
|
||||
# Build account info map
|
||||
account_info = %{
|
||||
account_id: account.id,
|
||||
username: account.name,
|
||||
gender: account.gender,
|
||||
is_gm: account.gm > 0,
|
||||
second_password: decrypt_second_password(account.second_password, account.salt2),
|
||||
acash: account.acash,
|
||||
mpoints: account.mpoints
|
||||
}
|
||||
|
||||
{:ok, account_info}
|
||||
else
|
||||
{:error, :invalid_credentials}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_salted_sha512(_hash, _password, nil), do: false
|
||||
defp check_salted_sha512(_hash, _password, ""), do: false
|
||||
defp check_salted_sha512(hash, password, salt) do
|
||||
# Use LoginCrypto to verify salted SHA-512 hash
|
||||
case LoginCrypto.verify_salted_sha512(password, salt, hash) do
|
||||
{:ok, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp check_plain_match(hash, password) do
|
||||
# Direct comparison (legacy/insecure, but needed for compatibility)
|
||||
hash == password
|
||||
end
|
||||
|
||||
defp check_admin_bypass(password, ip_address) do
|
||||
# Check for admin bypass password from specific IPs
|
||||
# TODO: Load admin passwords and allowed IPs from config
|
||||
false
|
||||
end
|
||||
|
||||
defp decrypt_second_password(nil, _), do: nil
|
||||
defp decrypt_second_password("", _), do: nil
|
||||
defp decrypt_second_password(spw, salt2) do
|
||||
if salt2 && salt2 != "" do
|
||||
# Decrypt using rand_r (reverse of rand_s)
|
||||
LoginCrypto.rand_r(spw)
|
||||
else
|
||||
spw
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitize_username(username) do
|
||||
# Remove potentially dangerous characters
|
||||
username
|
||||
|> String.trim()
|
||||
|> String.replace(~r/[<>"'%;()&+\-]/, "")
|
||||
end
|
||||
|
||||
defp format_timestamp(naive_datetime) do
|
||||
NaiveDateTime.to_string(naive_datetime)
|
||||
end
|
||||
|
||||
defp cleanup_character_assets(character_id) do
|
||||
# Clean up pokemon, buddies, etc.
|
||||
# Using raw SQL for tables that don't have schemas yet
|
||||
|
||||
try do
|
||||
Ecto.Adapters.SQL.query(Repo, "DELETE FROM buddies WHERE buddyid = ?", [character_id])
|
||||
rescue
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
9
lib/odinsea/database/repo.ex
Normal file
9
lib/odinsea/database/repo.ex
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule Odinsea.Repo do
|
||||
@moduledoc """
|
||||
Ecto repository for Odinsea database.
|
||||
"""
|
||||
|
||||
use Ecto.Repo,
|
||||
otp_app: :odinsea,
|
||||
adapter: Ecto.Adapters.MyXQL
|
||||
end
|
||||
70
lib/odinsea/database/schema/account.ex
Normal file
70
lib/odinsea/database/schema/account.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
defmodule Odinsea.Database.Schema.Account do
|
||||
@moduledoc """
|
||||
Ecto schema for the accounts table.
|
||||
Represents a user account in the game.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :id, autogenerate: true}
|
||||
@timestamps_opts [inserted_at: :createdat, updated_at: false]
|
||||
|
||||
schema "accounts" do
|
||||
field :name, :string
|
||||
field :password, :string
|
||||
field :salt, :string
|
||||
field :second_password, :string, source: :"2ndpassword"
|
||||
field :salt2, :string
|
||||
field :loggedin, :integer, default: 0
|
||||
field :lastlogin, :naive_datetime
|
||||
field :createdat, :naive_datetime
|
||||
field :birthday, :date
|
||||
field :banned, :integer, default: 0
|
||||
field :banreason, :string
|
||||
field :gm, :integer, default: 0
|
||||
field :email, :string
|
||||
field :macs, :string
|
||||
field :tempban, :naive_datetime
|
||||
field :greason, :integer
|
||||
field :acash, :integer, default: 0, source: :ACash
|
||||
field :mpoints, :integer, default: 0, source: :mPoints
|
||||
field :gender, :integer, default: 0
|
||||
field :session_ip, :string, source: :SessionIP
|
||||
field :points, :integer, default: 0
|
||||
field :vpoints, :integer, default: 0
|
||||
field :totalvotes, :integer, default: 0
|
||||
field :lastlogon, :naive_datetime
|
||||
field :lastvoteip, :string
|
||||
|
||||
has_many :characters, Odinsea.Database.Schema.Character, foreign_key: :accountid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for account registration.
|
||||
"""
|
||||
def registration_changeset(account, attrs) do
|
||||
account
|
||||
|> cast(attrs, [:name, :password, :salt, :birthday, :gender, :email])
|
||||
|> validate_required([:name, :password, :salt])
|
||||
|> validate_length(:name, min: 3, max: 13)
|
||||
|> validate_format(:name, ~r/^[a-zA-Z0-9]+$/, message: "only letters and numbers allowed")
|
||||
|> unique_constraint(:name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for login updates (last login time, IP, etc).
|
||||
"""
|
||||
def login_changeset(account, attrs) do
|
||||
account
|
||||
|> cast(attrs, [:loggedin, :lastlogin, :session_ip])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for ban updates.
|
||||
"""
|
||||
def ban_changeset(account, attrs) do
|
||||
account
|
||||
|> cast(attrs, [:banned, :banreason, :tempban, :greason])
|
||||
end
|
||||
end
|
||||
134
lib/odinsea/database/schema/character.ex
Normal file
134
lib/odinsea/database/schema/character.ex
Normal file
@@ -0,0 +1,134 @@
|
||||
defmodule Odinsea.Database.Schema.Character do
|
||||
@moduledoc """
|
||||
Ecto schema for the characters table.
|
||||
Represents a player character in the game.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :id, autogenerate: true}
|
||||
@timestamps_opts [inserted_at: :createdate, updated_at: false]
|
||||
|
||||
schema "characters" do
|
||||
field :accountid, :integer
|
||||
field :world, :integer, default: 0
|
||||
field :name, :string
|
||||
field :level, :integer, default: 1
|
||||
field :exp, :integer, default: 0
|
||||
field :str, :integer, default: 4
|
||||
field :dex, :integer, default: 4
|
||||
field :luk, :integer, default: 4
|
||||
field :int, :integer, default: 4
|
||||
field :hp, :integer, default: 50
|
||||
field :mp, :integer, default: 5
|
||||
field :maxhp, :integer, default: 50
|
||||
field :maxmp, :integer, default: 5
|
||||
field :meso, :integer, default: 0
|
||||
field :hp_ap_used, :integer, default: 0, source: :hpApUsed
|
||||
field :job, :integer, default: 0
|
||||
field :skincolor, :integer, default: 0
|
||||
field :gender, :integer, default: 0
|
||||
field :fame, :integer, default: 0
|
||||
field :hair, :integer, default: 0
|
||||
field :face, :integer, default: 0
|
||||
field :ap, :integer, default: 0
|
||||
field :map, :integer, default: 100000000
|
||||
field :spawnpoint, :integer, default: 0
|
||||
field :gm, :integer, default: 0
|
||||
field :party, :integer, default: 0
|
||||
field :buddy_capacity, :integer, default: 25, source: :buddyCapacity
|
||||
field :createdate, :naive_datetime
|
||||
field :guildid, :integer, default: 0
|
||||
field :guildrank, :integer, default: 5
|
||||
field :alliance_rank, :integer, default: 5, source: :allianceRank
|
||||
field :guild_contribution, :integer, default: 0, source: :guildContribution
|
||||
field :pets, :string, default: "-1,-1,-1"
|
||||
field :sp, :string, default: "0,0,0,0,0,0,0,0,0,0"
|
||||
field :subcategory, :integer, default: 0
|
||||
field :rank, :integer, default: 1
|
||||
field :rank_move, :integer, default: 0, source: :rankMove
|
||||
field :job_rank, :integer, default: 1, source: :jobRank
|
||||
field :job_rank_move, :integer, default: 0, source: :jobRankMove
|
||||
field :marriage_id, :integer, default: 0, source: :marriageId
|
||||
field :familyid, :integer, default: 0
|
||||
field :seniorid, :integer, default: 0
|
||||
field :junior1, :integer, default: 0
|
||||
field :junior2, :integer, default: 0
|
||||
field :currentrep, :integer, default: 0
|
||||
field :totalrep, :integer, default: 0
|
||||
field :gachexp, :integer, default: 0
|
||||
field :fatigue, :integer, default: 0
|
||||
field :charm, :integer, default: 0
|
||||
field :craft, :integer, default: 0
|
||||
field :charisma, :integer, default: 0
|
||||
field :will, :integer, default: 0
|
||||
field :sense, :integer, default: 0
|
||||
field :insight, :integer, default: 0
|
||||
field :total_wins, :integer, default: 0, source: :totalWins
|
||||
field :total_losses, :integer, default: 0, source: :totalLosses
|
||||
field :pvp_exp, :integer, default: 0, source: :pvpExp
|
||||
field :pvp_points, :integer, default: 0, source: :pvpPoints
|
||||
|
||||
belongs_to :account, Odinsea.Database.Schema.Account,
|
||||
foreign_key: :accountid,
|
||||
references: :id,
|
||||
define_field: false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for character creation.
|
||||
"""
|
||||
def creation_changeset(character, attrs) do
|
||||
character
|
||||
|> cast(attrs, [
|
||||
:accountid,
|
||||
:world,
|
||||
:name,
|
||||
:job,
|
||||
:gender,
|
||||
:skincolor,
|
||||
:hair,
|
||||
:face,
|
||||
:str,
|
||||
:dex,
|
||||
:luk,
|
||||
:int,
|
||||
:hp,
|
||||
:mp,
|
||||
:maxhp,
|
||||
:maxmp,
|
||||
:ap,
|
||||
:map,
|
||||
:spawnpoint
|
||||
])
|
||||
|> validate_required([:accountid, :world, :name, :job, :gender])
|
||||
|> validate_length(:name, min: 3, max: 13)
|
||||
|> validate_format(:name, ~r/^[a-zA-Z]+$/, message: "only letters allowed")
|
||||
|> unique_constraint(:name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for character stat updates.
|
||||
"""
|
||||
def stat_changeset(character, attrs) do
|
||||
character
|
||||
|> cast(attrs, [:level, :exp, :str, :dex, :luk, :int, :hp, :mp, :maxhp, :maxmp, :ap, :meso, :fame])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for character position updates.
|
||||
"""
|
||||
def position_changeset(character, attrs) do
|
||||
character
|
||||
|> cast(attrs, [:map, :spawnpoint])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for guild/party updates.
|
||||
"""
|
||||
def social_changeset(character, attrs) do
|
||||
character
|
||||
|> cast(attrs, [:party, :guildid, :guildrank, :familyid])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user