kimi gone wild
This commit is contained in:
459
lib/odinsea/game/quest_progress.ex
Normal file
459
lib/odinsea/game/quest_progress.ex
Normal file
@@ -0,0 +1,459 @@
|
||||
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
|
||||
Reference in New Issue
Block a user