460 lines
13 KiB
Elixir
460 lines
13 KiB
Elixir
defmodule Odinsea.Game.QuestProgress do
|
|
@moduledoc """
|
|
Player Quest Progress tracking module.
|
|
|
|
Tracks individual player's quest states:
|
|
- Quest status (not started, in progress, completed)
|
|
- Mob kill counts for active quests
|
|
- Custom quest data (for scripted quests)
|
|
- Forfeiture count
|
|
- Completion time
|
|
- NPC ID (for quest tracking)
|
|
|
|
## Quest Status
|
|
|
|
- `0` - Not started
|
|
- `1` - In progress
|
|
- `2` - Completed
|
|
|
|
## Progress Structure
|
|
|
|
Each quest progress entry contains:
|
|
- Quest ID
|
|
- Status (0/1/2)
|
|
- Mob kills (map of mob_id => count)
|
|
- Forfeited count
|
|
- Completion time (timestamp)
|
|
- Custom data (string for scripted quests)
|
|
- NPC ID (related NPC for the quest)
|
|
"""
|
|
|
|
alias Odinsea.Game.Quest
|
|
|
|
defmodule ProgressEntry do
|
|
@moduledoc "Individual quest progress entry"
|
|
|
|
@type t :: %__MODULE__{
|
|
quest_id: integer(),
|
|
status: integer(),
|
|
mob_kills: %{integer() => integer()},
|
|
forfeited: integer(),
|
|
completion_time: integer() | nil,
|
|
custom_data: String.t() | nil,
|
|
npc_id: integer() | nil
|
|
}
|
|
|
|
defstruct [
|
|
:quest_id,
|
|
:status,
|
|
mob_kills: %{},
|
|
forfeited: 0,
|
|
completion_time: nil,
|
|
custom_data: nil,
|
|
npc_id: nil
|
|
]
|
|
end
|
|
|
|
@type t :: %__MODULE__{
|
|
character_id: integer(),
|
|
quests: %{integer() => ProgressEntry.t()}
|
|
}
|
|
|
|
defstruct [
|
|
:character_id,
|
|
quests: %{}
|
|
]
|
|
|
|
## Public API
|
|
|
|
@doc "Creates a new empty quest progress for a character"
|
|
@spec new(integer()) :: t()
|
|
def new(character_id) do
|
|
%__MODULE__{
|
|
character_id: character_id,
|
|
quests: %{}
|
|
}
|
|
end
|
|
|
|
@doc "Gets a quest's progress entry"
|
|
@spec get_quest(t(), integer()) :: ProgressEntry.t() | nil
|
|
def get_quest(%__MODULE__{} = progress, quest_id) do
|
|
Map.get(progress.quests, quest_id)
|
|
end
|
|
|
|
@doc "Gets the status of a quest"
|
|
@spec get_status(t(), integer()) :: integer()
|
|
def get_status(%__MODULE__{} = progress, quest_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil -> 0
|
|
entry -> entry.status
|
|
end
|
|
end
|
|
|
|
@doc "Checks if a quest is in progress"
|
|
@spec in_progress?(t(), integer()) :: boolean()
|
|
def in_progress?(%__MODULE__{} = progress, quest_id) do
|
|
get_status(progress, quest_id) == 1
|
|
end
|
|
|
|
@doc "Checks if a quest is completed"
|
|
@spec completed?(t(), integer()) :: boolean()
|
|
def completed?(%__MODULE__{} = progress, quest_id) do
|
|
get_status(progress, quest_id) == 2
|
|
end
|
|
|
|
@doc "Checks if a quest can be started"
|
|
@spec can_start?(t(), integer()) :: boolean()
|
|
def can_start?(%__MODULE__{} = progress, quest_id) do
|
|
status = get_status(progress, quest_id)
|
|
|
|
case Quest.get_quest(quest_id) do
|
|
nil ->
|
|
# Unknown quest, can't start
|
|
false
|
|
|
|
quest ->
|
|
# Can start if:
|
|
# 1. Status is 0 (not started), OR
|
|
# 2. Status is 2 (completed) AND quest is repeatable
|
|
status == 0 || (status == 2 && quest.repeatable)
|
|
end
|
|
end
|
|
|
|
@doc "Starts a quest"
|
|
@spec start_quest(t(), integer(), integer() | nil) :: t()
|
|
def start_quest(%__MODULE__{} = progress, quest_id, npc_id \\ nil) do
|
|
now = System.system_time(:second)
|
|
|
|
entry = %ProgressEntry{
|
|
quest_id: quest_id,
|
|
status: 1,
|
|
npc_id: npc_id,
|
|
mob_kills: %{},
|
|
completion_time: now
|
|
}
|
|
|
|
update_quest_entry(progress, entry)
|
|
end
|
|
|
|
@doc "Completes a quest"
|
|
@spec complete_quest(t(), integer(), integer() | nil) :: t()
|
|
def complete_quest(%__MODULE__{} = progress, quest_id, npc_id \\ nil) do
|
|
now = System.system_time(:second)
|
|
|
|
entry =
|
|
case get_quest(progress, quest_id) do
|
|
nil ->
|
|
%ProgressEntry{
|
|
quest_id: quest_id,
|
|
status: 2,
|
|
npc_id: npc_id,
|
|
completion_time: now
|
|
}
|
|
|
|
existing ->
|
|
%ProgressEntry{
|
|
existing
|
|
| status: 2,
|
|
npc_id: npc_id,
|
|
completion_time: now
|
|
}
|
|
end
|
|
|
|
update_quest_entry(progress, entry)
|
|
end
|
|
|
|
@doc "Forfeits a quest (abandons it)"
|
|
@spec forfeit_quest(t(), integer()) :: t()
|
|
def forfeit_quest(%__MODULE__{} = progress, quest_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil ->
|
|
# Quest not started, nothing to forfeit
|
|
progress
|
|
|
|
entry when entry.status == 1 ->
|
|
# Quest is in progress, forfeit it
|
|
forfeited = entry.forfeited + 1
|
|
|
|
updated_entry = %ProgressEntry{
|
|
quest_id: quest_id,
|
|
status: 0,
|
|
forfeited: forfeited,
|
|
completion_time: entry.completion_time,
|
|
custom_data: nil,
|
|
mob_kills: %{}
|
|
}
|
|
|
|
update_quest_entry(progress, updated_entry)
|
|
|
|
_entry ->
|
|
# Quest not in progress, can't forfeit
|
|
progress
|
|
end
|
|
end
|
|
|
|
@doc "Resets a quest to not started state"
|
|
@spec reset_quest(t(), integer()) :: t()
|
|
def reset_quest(%__MODULE__{} = progress, quest_id) do
|
|
%{progress | quests: Map.delete(progress.quests, quest_id)}
|
|
end
|
|
|
|
@doc "Records a mob kill for an active quest"
|
|
@spec record_mob_kill(t(), integer(), integer()) :: t()
|
|
def record_mob_kill(%__MODULE__{} = progress, quest_id, mob_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil ->
|
|
# Quest not started
|
|
progress
|
|
|
|
entry when entry.status != 1 ->
|
|
# Quest not in progress
|
|
progress
|
|
|
|
entry ->
|
|
# Check if this mob is relevant to the quest
|
|
case Quest.get_relevant_mobs(quest_id) do
|
|
%{^mob_id => required_count} ->
|
|
current_count = Map.get(entry.mob_kills, mob_id, 0)
|
|
|
|
# Only increment if not yet completed
|
|
new_count = min(current_count + 1, required_count)
|
|
|
|
updated_mob_kills = Map.put(entry.mob_kills, mob_id, new_count)
|
|
updated_entry = %ProgressEntry{entry | mob_kills: updated_mob_kills}
|
|
|
|
update_quest_entry(progress, updated_entry)
|
|
|
|
_ ->
|
|
# Mob not relevant to this quest
|
|
progress
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc "Gets mob kill count for a quest"
|
|
@spec get_mob_kills(t(), integer(), integer()) :: integer()
|
|
def get_mob_kills(%__MODULE__{} = progress, quest_id, mob_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil -> 0
|
|
entry -> Map.get(entry.mob_kills, mob_id, 0)
|
|
end
|
|
end
|
|
|
|
@doc "Sets custom data for a quest"
|
|
@spec set_custom_data(t(), integer(), String.t()) :: t()
|
|
def set_custom_data(%__MODULE__{} = progress, quest_id, data) do
|
|
case get_quest(progress, quest_id) do
|
|
nil ->
|
|
# Quest not started, create entry with custom data
|
|
entry = %ProgressEntry{
|
|
quest_id: quest_id,
|
|
status: 1,
|
|
custom_data: data
|
|
}
|
|
|
|
update_quest_entry(progress, entry)
|
|
|
|
entry ->
|
|
updated_entry = %ProgressEntry{entry | custom_data: data}
|
|
update_quest_entry(progress, updated_entry)
|
|
end
|
|
end
|
|
|
|
@doc "Gets custom data for a quest"
|
|
@spec get_custom_data(t(), integer()) :: String.t() | nil
|
|
def get_custom_data(%__MODULE__{} = progress, quest_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil -> nil
|
|
entry -> entry.custom_data
|
|
end
|
|
end
|
|
|
|
@doc "Sets the NPC ID for a quest"
|
|
@spec set_npc(t(), integer(), integer()) :: t()
|
|
def set_npc(%__MODULE__{} = progress, quest_id, npc_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil ->
|
|
entry = %ProgressEntry{
|
|
quest_id: quest_id,
|
|
status: 0,
|
|
npc_id: npc_id
|
|
}
|
|
|
|
update_quest_entry(progress, entry)
|
|
|
|
entry ->
|
|
updated_entry = %ProgressEntry{entry | npc_id: npc_id}
|
|
update_quest_entry(progress, updated_entry)
|
|
end
|
|
end
|
|
|
|
@doc "Gets the NPC ID for a quest"
|
|
@spec get_npc(t(), integer()) :: integer() | nil
|
|
def get_npc(%__MODULE__{} = progress, quest_id) do
|
|
case get_quest(progress, quest_id) do
|
|
nil -> nil
|
|
entry -> entry.npc_id
|
|
end
|
|
end
|
|
|
|
@doc "Gets all active (in-progress) quests"
|
|
@spec get_active_quests(t()) :: [ProgressEntry.t()]
|
|
def get_active_quests(%__MODULE__{} = progress) do
|
|
progress.quests
|
|
|> Map.values()
|
|
|> Enum.filter(fn entry -> entry.status == 1 end)
|
|
end
|
|
|
|
@doc "Gets all completed quests"
|
|
@spec get_completed_quests(t()) :: [ProgressEntry.t()]
|
|
def get_completed_quests(%__MODULE__{} = progress) do
|
|
progress.quests
|
|
|> Map.values()
|
|
|> Enum.filter(fn entry -> entry.status == 2 end)
|
|
end
|
|
|
|
@doc "Gets count of completed quests"
|
|
@spec get_completed_count(t()) :: integer()
|
|
def get_completed_count(%__MODULE__{} = progress) do
|
|
progress.quests
|
|
|> Map.values()
|
|
|> Enum.count(fn entry -> entry.status == 2 end)
|
|
end
|
|
|
|
@doc "Checks if a quest can be repeated (interval passed)"
|
|
@spec can_repeat?(t(), integer()) :: boolean()
|
|
def can_repeat?(%__MODULE__{} = progress, quest_id) do
|
|
case Quest.get_quest(quest_id) do
|
|
nil -> false
|
|
quest ->
|
|
if not quest.repeatable do
|
|
false
|
|
else
|
|
case get_quest(progress, quest_id) do
|
|
nil -> true
|
|
entry ->
|
|
case entry.completion_time do
|
|
nil -> true
|
|
last_completion ->
|
|
# Check interval requirement
|
|
interval_req =
|
|
Enum.find(quest.complete_requirements, fn req ->
|
|
req.type == :interval
|
|
end)
|
|
|
|
interval_seconds =
|
|
case interval_req do
|
|
nil -> 0
|
|
req -> req.data * 60 # Convert minutes to seconds
|
|
end
|
|
|
|
now = System.system_time(:second)
|
|
(now - last_completion) >= interval_seconds
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc "Converts progress to a map for database storage"
|
|
@spec to_map(t()) :: map()
|
|
def to_map(%__MODULE__{} = progress) do
|
|
%{
|
|
character_id: progress.character_id,
|
|
quests:
|
|
Enum.into(progress.quests, %{}, fn {quest_id, entry} ->
|
|
{quest_id, entry_to_map(entry)}
|
|
end)
|
|
}
|
|
end
|
|
|
|
@doc "Creates progress from a map (database deserialization)"
|
|
@spec from_map(map()) :: t()
|
|
def from_map(map) do
|
|
character_id = Map.get(map, :character_id, Map.get(map, "character_id", 0))
|
|
|
|
quests =
|
|
map
|
|
|> Map.get(:quests, Map.get(map, "quests", %{}))
|
|
|> Enum.into(%{}, fn {quest_id_str, entry_data} ->
|
|
quest_id =
|
|
if is_binary(quest_id_str) do
|
|
String.to_integer(quest_id_str)
|
|
else
|
|
quest_id_str
|
|
end
|
|
|
|
{quest_id, entry_from_map(entry_data)}
|
|
end)
|
|
|
|
%__MODULE__{
|
|
character_id: character_id,
|
|
quests: quests
|
|
}
|
|
end
|
|
|
|
@doc "Merges progress from database with current state"
|
|
@spec merge(t(), t()) :: t()
|
|
def merge(%__MODULE__{} = current, %__MODULE__{} = loaded) do
|
|
# Prefer loaded data for completed quests
|
|
# Keep current data for in-progress quests if newer
|
|
|
|
merged_quests =
|
|
Map.merge(loaded.quests, current.quests, fn _quest_id, loaded_entry, current_entry ->
|
|
cond do
|
|
loaded_entry.status == 2 and current_entry.status != 2 ->
|
|
# Keep completed status from loaded
|
|
loaded_entry
|
|
|
|
current_entry.status == 2 and loaded_entry.status != 2 ->
|
|
# Newly completed
|
|
current_entry
|
|
|
|
current_entry.completion_time && loaded_entry.completion_time ->
|
|
if current_entry.completion_time > loaded_entry.completion_time do
|
|
current_entry
|
|
else
|
|
loaded_entry
|
|
end
|
|
|
|
true ->
|
|
# Default to current
|
|
current_entry
|
|
end
|
|
end)
|
|
|
|
%__MODULE__{current | quests: merged_quests}
|
|
end
|
|
|
|
## Private Functions
|
|
|
|
defp update_quest_entry(%__MODULE__{} = progress, %ProgressEntry{} = entry) do
|
|
updated_quests = Map.put(progress.quests, entry.quest_id, entry)
|
|
%{progress | quests: updated_quests}
|
|
end
|
|
|
|
defp entry_to_map(%ProgressEntry{} = entry) do
|
|
%{
|
|
quest_id: entry.quest_id,
|
|
status: entry.status,
|
|
mob_kills: entry.mob_kills,
|
|
forfeited: entry.forfeited,
|
|
completion_time: entry.completion_time,
|
|
custom_data: entry.custom_data,
|
|
npc_id: entry.npc_id
|
|
}
|
|
end
|
|
|
|
defp entry_from_map(map) do
|
|
%ProgressEntry{
|
|
quest_id: Map.get(map, :quest_id, Map.get(map, "quest_id", 0)),
|
|
status: Map.get(map, :status, Map.get(map, "status", 0)),
|
|
mob_kills: Map.get(map, :mob_kills, Map.get(map, "mob_kills", %{})),
|
|
forfeited: Map.get(map, :forfeited, Map.get(map, "forfeited", 0)),
|
|
completion_time: Map.get(map, :completion_time, Map.get(map, "completion_time", nil)),
|
|
custom_data: Map.get(map, :custom_data, Map.get(map, "custom_data", nil)),
|
|
npc_id: Map.get(map, :npc_id, Map.get(map, "npc_id", nil))
|
|
}
|
|
end
|
|
end
|