Packages

SDK for GameServer hooks development. Provides type specs, documentation, and IDE autocomplete for GameServer modules without requiring the full server.

Current section

Files

Jump to
game_server_sdk lib game_server quests.ex
Raw

lib/game_server/quests.ex

defmodule GameServer.Quests do
@moduledoc ~S"""
Event-driven quest/progression engine.
One engine, three independent dimensions: a **reset** cycle (never / daily /
weekly / monthly / every N days), an optional **window**
(`starts_at`/`ends_at`), and an optional **prerequisite**
(`prerequisite_quest_key`). Any combination works — a biweekly quest inside
a seasonal window that also requires an earlier quest is just those three
fields set. Rewards pay into `GameServer.Economy` / `GameServer.Inventory`
exactly once. `category` is a free-form label for your UI only.
## Reporting progress (server-side / hooks)
Quests.report_event(user_id, "enemy_killed", 1, %{"map" => "desert"})
Every **active** quest with an objective on `"enemy_killed"` (whose `params`
all match the meta) advances; a quest completes when every objective meets
its target. There is deliberately **no public endpoint** for this — clients
cannot advance their own quests. Core wires common events; games call it
from their hooks for custom events.
## Claiming
{:ok, %{progress: progress, rewards: rewards}} = Quests.claim(user_id, "daily_win_3")
Claiming is gated by an atomic `completed → claimed` status transition, so a
double-tap or a concurrent claim can't double-pay. Rewards are granted after
the transition with a per-entry idempotency key (`"quest:<progress_id>:<i>"`),
so a crashed or retried grant can't double-apply either; rows that claimed
but never finished granting are healed by `recover_pending_rewards/1`.
Quests with `auto_claim` grant immediately on completion (skipping the
`before_quest_claim` hook — there is no player request to veto).
## Resets
`period_key` is derived from **UTC time** by the quest's reset (daily →
`"2026-07-22"`, weekly → `"2026-W30"`, monthly → `"2026-07"`, interval →
`"I14-1436"`, never → `"static"`). A new period simply means a new progress
row on the next reported event — nothing needs to fire at midnight, and
state resolves correctly even if no job ever runs.
**Note:** This is an SDK stub. Calling these functions will raise an error.
The actual implementation runs on the GameServer.
"""
@type user_id() :: Ecto.UUID.t()
@doc ~S"""
All active quest definitions (cached — this backs event dispatch).
"""
@spec active_quests() :: [GameServer.Quests.Quest.t()]
def active_quests() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Quests.active_quests/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Claim on a user's behalf, skipping the `before_quest_claim` veto (admin).
"""
@spec admin_claim(user_id(), String.t()) ::
{:ok, %{progress: GameServer.Quests.QuestProgress.t(), rewards: [map()]}} | {:error, term()}
def admin_claim(_user_id, _quest_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %{progress: %GameServer.Quests.QuestProgress{id: "", user_id: "", quest_key: "", period_key: "static", objective_progress: %{}, status: "active", completed_at: nil, claimed_at: nil, rewards_granted_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}, rewards: []}}
_ ->
raise "GameServer.Quests.admin_claim/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Force-complete a quest for a user (admin grant): every objective jumps to
its target and the normal completion side effects fire (hooks, auto-claim).
"""
@spec admin_complete(user_id(), String.t()) ::
{:ok, GameServer.Quests.QuestProgress.t()} | {:error, term()}
def admin_complete(_user_id, _quest_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Quests.QuestProgress{id: "", user_id: "", quest_key: "", period_key: "static", objective_progress: %{}, status: "active", completed_at: nil, claimed_at: nil, rewards_granted_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Quests.admin_complete/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Delete a user's current-period progress row for a quest (admin reset).
"""
@spec admin_reset(user_id(), String.t()) ::
{:ok, GameServer.Quests.QuestProgress.t() | :not_found} | {:error, term()}
def admin_reset(_user_id, _quest_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, nil}
_ ->
raise "GameServer.Quests.admin_reset/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Returns a changeset for tracking quest changes (used by forms).
"""
@spec change_quest(GameServer.Quests.Quest.t(), map()) :: Ecto.Changeset.t()
def change_quest(_quest, _attrs) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
nil
_ ->
raise "GameServer.Quests.change_quest/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Claim a completed quest's rewards for the current period.
Runs the `before_quest_claim` pipeline hook (veto), then transitions
`completed → claimed` atomically — only the winner grants rewards.
Returns `{:ok, %{progress: progress, rewards: rewards}}` or
`{:error, :quest_not_found | :not_completed | :already_claimed | term()}`.
"""
@spec claim(user_id(), String.t(), keyword()) ::
{:ok, %{progress: GameServer.Quests.QuestProgress.t(), rewards: [map()]}} | {:error, term()}
def claim(_user_id, _quest_key, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %{progress: %GameServer.Quests.QuestProgress{id: "", user_id: "", quest_key: "", period_key: "static", objective_progress: %{}, status: "active", completed_at: nil, claimed_at: nil, rewards_granted_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}, rewards: []}}
_ ->
raise "GameServer.Quests.claim/3 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Number of completed-but-unclaimed quests for a user (badge count).
"""
@spec claimable_count(user_id()) :: non_neg_integer()
def claimable_count(_user_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.claimable_count/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count progress rows (same filters as `list_progress/1`).
"""
@spec count_progress(keyword()) :: non_neg_integer()
def count_progress(_opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.count_progress/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count quest definitions (same filters as `list_quests/1`).
"""
@spec count_quests(keyword()) :: non_neg_integer()
def count_quests(_opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.count_quests/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count of a user's completed quests (same filters as `list_user_completions/2`).
"""
@spec count_user_completions(
user_id(),
keyword()
) :: non_neg_integer()
def count_user_completions(_user_id, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.count_user_completions/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count of quests visible to the user (same filters as `list_user_quests/2`).
"""
@spec count_user_quests(
user_id(),
keyword()
) :: non_neg_integer()
def count_user_quests(_user_id, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.count_user_quests/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Creates a quest definition. Capped by the `max_quests` limit.
"""
@spec create_quest(map()) :: {:ok, GameServer.Quests.Quest.t()} | {:error, term()}
def create_quest(_attrs) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Quests.Quest{id: "", key: "", title: "", description: "", icon_url: nil, sort_order: 0, hidden: false, kind: "achievement", objectives: [], rewards: [], auto_claim: false, prerequisite_quest_key: nil, starts_at: nil, ends_at: nil, active: true, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Quests.create_quest/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Quest statistics for the admin dashboard.
"""
@spec dashboard_stats() :: map()
def dashboard_stats() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
%{}
_ ->
raise "GameServer.Quests.dashboard_stats/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Deletes a quest definition and all related progress.
"""
@spec delete_quest(GameServer.Quests.Quest.t()) ::
{:ok, GameServer.Quests.Quest.t()} | {:error, Ecto.Changeset.t()}
def delete_quest(_quest) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Quests.Quest{id: "", key: "", title: "", description: "", icon_url: nil, sort_order: 0, hidden: false, kind: "achievement", objectives: [], rewards: [], auto_claim: false, prerequisite_quest_key: nil, starts_at: nil, ends_at: nil, active: true, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Quests.delete_quest/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Per-status progress counts for one quest (admin completion funnel).
"""
@spec funnel(String.t()) :: %{required(String.t()) => non_neg_integer()}
def funnel(_quest_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.funnel/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Get a user's progress row for a quest's current period.
"""
@spec get_progress(user_id(), String.t()) :: GameServer.Quests.QuestProgress.t() | nil
def get_progress(_user_id, _quest_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
if :erlang.phash2(make_ref(), 2) == 0, do: nil, else: %GameServer.Quests.QuestProgress{id: "", user_id: "", quest_key: "", period_key: "static", objective_progress: %{}, status: "active", completed_at: nil, claimed_at: nil, rewards_granted_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}
_ ->
raise "GameServer.Quests.get_progress/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Get a quest by ID.
"""
@spec get_quest(Ecto.UUID.t()) :: GameServer.Quests.Quest.t() | nil
def get_quest(_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
if :erlang.phash2(make_ref(), 2) == 0, do: nil, else: %GameServer.Quests.Quest{id: "", key: "", title: "", description: "", icon_url: nil, sort_order: 0, hidden: false, kind: "achievement", objectives: [], rewards: [], auto_claim: false, prerequisite_quest_key: nil, starts_at: nil, ends_at: nil, active: true, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}
_ ->
raise "GameServer.Quests.get_quest/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Get a quest by key.
"""
@spec get_quest_by_key(String.t()) :: GameServer.Quests.Quest.t() | nil
def get_quest_by_key(_key) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
if :erlang.phash2(make_ref(), 2) == 0, do: nil, else: %GameServer.Quests.Quest{id: "", key: "", title: "", description: "", icon_url: nil, sort_order: 0, hidden: false, kind: "achievement", objectives: [], rewards: [], auto_claim: false, prerequisite_quest_key: nil, starts_at: nil, ends_at: nil, active: true, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}
_ ->
raise "GameServer.Quests.get_quest_by_key/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Lists progress rows (admin viewer).
## Options
- `:user_id` — exact UUID or username/display-name substring
- `:quest_key`, `:status`
- `:page` / `:page_size`
"""
@spec list_progress(keyword()) :: [GameServer.Quests.QuestProgress.t()]
def list_progress(_opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Quests.list_progress/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Lists quest definitions (admin view — no per-user state).
## Options
- `:category` — filter by category
- `:active` — filter by active flag
- `:search` — substring match on key/title
- `:page` / `:page_size`
"""
@spec list_quests(keyword()) :: [GameServer.Quests.Quest.t()]
def list_quests(_opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Quests.list_quests/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
A user's completed quests, newest first — the public-profile view
("their achievements"). Hidden quests appear once earned.
## Options
- `:category` — filter by category (a profile typically wants `"achievement"`)
- `:page` / `:page_size`
"""
@spec list_user_completions(
user_id(),
keyword()
) :: [%{quest: GameServer.Quests.Quest.t(), progress: GameServer.Quests.QuestProgress.t()}]
def list_user_completions(_user_id, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
%{}
_ ->
raise "GameServer.Quests.list_user_completions/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Lists quests as seen by one user: active definitions in-window with the
user's current-period progress and a claimable flag.
Hidden quests are listed but carry no details until earned (callers obscure
them). Chain quests only appear once their prerequisite is met.
## Options
- `:category` — filter by category
- `:status` — `"in_progress"` (not yet completed), `"claimable"`
(completed, waiting to be claimed) or `"done"` (completed or claimed)
- `:page` / `:page_size`
"""
@spec list_user_quests(
user_id(),
keyword()
) :: [
%{
quest: GameServer.Quests.Quest.t(),
progress: GameServer.Quests.QuestProgress.t() | nil,
claimable: boolean()
}
]
def list_user_quests(_user_id, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Quests.list_user_quests/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
The reset bucket a quest is in at `now` (UTC).
`"static"` when it never resets, else the current day (`"2026-07-22"`),
ISO week (`"2026-W30"`), month (`"2026-07"`), or interval bucket
(`"I14-1436"` — the 1436th 14-day window since the epoch). Derived purely
from the clock, so a reset needs nothing to fire at midnight.
"""
@spec period_key(GameServer.Quests.Quest.t() | String.t(), DateTime.t()) :: String.t()
def period_key(_quest_or_reset, _now) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
""
_ ->
raise "GameServer.Quests.period_key/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
True when the quest's prerequisite (if any) is completed by the user.
"""
@spec prerequisite_met?(user_id(), GameServer.Quests.Quest.t()) :: boolean()
def prerequisite_met?(_user_id, _quest) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
false
_ ->
raise "GameServer.Quests.prerequisite_met?/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Deletes daily/weekly progress rows whose period ended more than
`max_quest_period_history` days ago (called from `GameServer.Retention`).
"""
@spec prune_old_periods() :: non_neg_integer()
def prune_old_periods() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.prune_old_periods/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Re-runs reward grants for rows that claimed but never finished granting
(e.g. the process died mid-grant). Safe to run anywhere, any time — the
per-entry idempotency keys dedupe. Pass `:user_id` to heal one user (done
lazily when they list their quests). Returns the number of rows retried.
"""
@spec recover_pending_rewards(keyword()) :: non_neg_integer()
def recover_pending_rewards(_opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Quests.recover_pending_rewards/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Report a gameplay event for a user, advancing every matching active quest.
`meta` narrows objective matching: an objective with `params` only advances
when every param key/value is present in `meta`.
Returns `{:ok, progress_rows}` for the quests that advanced.
"""
@spec report_event(user_id(), String.t(), pos_integer(), map()) ::
{:ok, [GameServer.Quests.QuestProgress.t()]}
def report_event(_user_id, _event, _amount, _meta) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, []}
_ ->
raise "GameServer.Quests.report_event/4 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Subscribe to global quest events (definition changes, completions).
"""
@spec subscribe_quests() :: :ok | {:error, term()}
def subscribe_quests() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
:ok
_ ->
raise "GameServer.Quests.subscribe_quests/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Updates a quest definition.
"""
@spec update_quest(GameServer.Quests.Quest.t(), map()) ::
{:ok, GameServer.Quests.Quest.t()} | {:error, Ecto.Changeset.t()}
def update_quest(_quest, _attrs) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Quests.Quest{id: "", key: "", title: "", description: "", icon_url: nil, sort_order: 0, hidden: false, kind: "achievement", objectives: [], rewards: [], auto_claim: false, prerequisite_quest_key: nil, starts_at: nil, ends_at: nil, active: true, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Quests.update_quest/2 is a stub - only available at runtime on GameServer"
end
end
end