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