745 lines
21 KiB
Elixir
745 lines
21 KiB
Elixir
defmodule Odinsea.Game.QuestAction do
|
|
@moduledoc """
|
|
Quest Action module - defines rewards and effects for quest completion.
|
|
|
|
Actions are executed when:
|
|
- Starting a quest (start_actions)
|
|
- Completing a quest (complete_actions)
|
|
|
|
## Action Types
|
|
|
|
- `:exp` - Experience points reward
|
|
- `:money` - Meso reward
|
|
- `:item` - Item rewards (can be job/gender restricted)
|
|
- `:pop` - Fame reward
|
|
- `:sp` - Skill points reward
|
|
- `:skill` - Learn specific skills
|
|
- `:nextQuest` - Start another quest automatically
|
|
- `:buffItemID` - Apply buff from item effect
|
|
- `:infoNumber` - Info quest update
|
|
- `:quest` - Update other quest states
|
|
|
|
## Trait EXP Types
|
|
|
|
- `:charmEXP` - Charm trait experience
|
|
- `:charismaEXP` - Charisma trait experience
|
|
- `:craftEXP` - Craft (smithing) trait experience
|
|
- `:insightEXP` - Insight trait experience
|
|
- `:senseEXP` - Sense trait experience
|
|
- `:willEXP` - Will trait experience
|
|
|
|
## Job Restrictions
|
|
|
|
Items and skills can be restricted by job using job encoding:
|
|
- Bit flags for job categories (Warrior, Magician, Bowman, Thief, Pirate, etc.)
|
|
- Supports both 5-byte and simple encodings
|
|
|
|
## Gender Restrictions
|
|
|
|
Items can be restricted by gender:
|
|
- `0` - Male only
|
|
- `1` - Female only
|
|
- `2` - Both (no restriction)
|
|
"""
|
|
|
|
alias Odinsea.Game.Quest
|
|
|
|
@type t :: %__MODULE__{
|
|
type: atom(),
|
|
value: any(),
|
|
applicable_jobs: [integer()],
|
|
int_store: integer()
|
|
}
|
|
|
|
defstruct [:type, :value, :applicable_jobs, :int_store]
|
|
|
|
defmodule QuestItem do
|
|
@moduledoc "Quest item reward structure"
|
|
|
|
@type t :: %__MODULE__{
|
|
item_id: integer(),
|
|
count: integer(),
|
|
period: integer(),
|
|
gender: integer(),
|
|
job: integer(),
|
|
job_ex: integer(),
|
|
prop: integer()
|
|
}
|
|
|
|
defstruct [
|
|
:item_id,
|
|
:count,
|
|
period: 0,
|
|
gender: 2,
|
|
job: -1,
|
|
job_ex: -1,
|
|
prop: -2
|
|
]
|
|
end
|
|
|
|
## Public API
|
|
|
|
@doc "Creates a new quest action"
|
|
@spec new(atom(), any(), keyword()) :: t()
|
|
def new(type, value, opts \\ []) do
|
|
%__MODULE__{
|
|
type: type,
|
|
value: value,
|
|
applicable_jobs: Keyword.get(opts, :applicable_jobs, []),
|
|
int_store: Keyword.get(opts, :int_store, 0)
|
|
}
|
|
end
|
|
|
|
@doc "Builds an action from a map (JSON deserialization)"
|
|
@spec from_map(map()) :: t()
|
|
def from_map(map) do
|
|
type = parse_type(Map.get(map, :type, Map.get(map, "type", "undefined")))
|
|
|
|
{value, applicable_jobs, int_store} = parse_action_data(type, map)
|
|
|
|
%__MODULE__{
|
|
type: type,
|
|
value: value,
|
|
applicable_jobs: applicable_jobs,
|
|
int_store: int_store
|
|
}
|
|
end
|
|
|
|
@doc "Parses a WZ action name into an atom"
|
|
@spec parse_type(String.t() | atom()) :: atom()
|
|
def parse_type(type) when is_atom(type), do: type
|
|
|
|
def parse_type(type_str) when is_binary(type_str) do
|
|
case String.downcase(type_str) do
|
|
"exp" -> :exp
|
|
"item" -> :item
|
|
"nextquest" -> :nextQuest
|
|
"money" -> :money
|
|
"quest" -> :quest
|
|
"skill" -> :skill
|
|
"pop" -> :pop
|
|
"buffitemid" -> :buffItemID
|
|
"infonumber" -> :infoNumber
|
|
"sp" -> :sp
|
|
"charismaexp" -> :charismaEXP
|
|
"charmexp" -> :charmEXP
|
|
"willexp" -> :willEXP
|
|
"insightexp" -> :insightEXP
|
|
"senseexp" -> :senseEXP
|
|
"craftexp" -> :craftEXP
|
|
"job" -> :job
|
|
_ -> :undefined
|
|
end
|
|
end
|
|
|
|
@doc "Runs start actions for a quest"
|
|
@spec run_start(t(), Odinsea.Game.Character.t()) :: Odinsea.Game.Character.t()
|
|
def run_start(%__MODULE__{} = action, character) do
|
|
do_run_start(action.type, action, character)
|
|
end
|
|
|
|
@doc "Runs end/complete actions for a quest"
|
|
@spec run_end(t(), Odinsea.Game.Character.t()) :: Odinsea.Game.Character.t()
|
|
def run_end(%__MODULE__{} = action, character) do
|
|
do_run_end(action.type, action, character)
|
|
end
|
|
|
|
@doc "Checks if character can receive this action's rewards (inventory space, etc.)"
|
|
@spec check_end(t(), Odinsea.Game.Character.t()) :: boolean()
|
|
def check_end(%__MODULE__{} = action, character) do
|
|
do_check_end(action.type, action, character)
|
|
end
|
|
|
|
@doc "Checks if an item reward can be given to this character"
|
|
@spec can_get_item?(QuestItem.t(), Odinsea.Game.Character.t()) :: boolean()
|
|
def can_get_item?(%QuestItem{} = item, character) do
|
|
# Check gender restriction
|
|
gender_ok =
|
|
if item.gender != 2 && item.gender >= 0 do
|
|
character_gender = Map.get(character, :gender, 0)
|
|
item.gender == character_gender
|
|
else
|
|
true
|
|
end
|
|
|
|
if not gender_ok do
|
|
false
|
|
else
|
|
# Check job restriction
|
|
if item.job > 0 do
|
|
character_job = Map.get(character, :job, 0)
|
|
job_codes = get_job_by_5byte_encoding(item.job)
|
|
|
|
job_found =
|
|
Enum.any?(job_codes, fn code ->
|
|
div(code, 100) == div(character_job, 100)
|
|
end)
|
|
|
|
if not job_found and item.job_ex > 0 do
|
|
job_codes_ex = get_job_by_simple_encoding(item.job_ex)
|
|
|
|
job_found =
|
|
Enum.any?(job_codes_ex, fn code ->
|
|
rem(div(code, 100), 10) == rem(div(character_job, 100), 10)
|
|
end)
|
|
end
|
|
|
|
job_found
|
|
else
|
|
true
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc "Gets job list from 5-byte encoding"
|
|
@spec get_job_by_5byte_encoding(integer()) :: [integer()]
|
|
def get_job_by_5byte_encoding(encoded) do
|
|
[]
|
|
|> add_job_if(encoded, 0x1, 0)
|
|
|> add_job_if(encoded, 0x2, 100)
|
|
|> add_job_if(encoded, 0x4, 200)
|
|
|> add_job_if(encoded, 0x8, 300)
|
|
|> add_job_if(encoded, 0x10, 400)
|
|
|> add_job_if(encoded, 0x20, 500)
|
|
|> add_job_if(encoded, 0x400, 1000)
|
|
|> add_job_if(encoded, 0x800, 1100)
|
|
|> add_job_if(encoded, 0x1000, 1200)
|
|
|> add_job_if(encoded, 0x2000, 1300)
|
|
|> add_job_if(encoded, 0x4000, 1400)
|
|
|> add_job_if(encoded, 0x8000, 1500)
|
|
|> add_job_if(encoded, 0x20000, 2001)
|
|
|> add_job_if(encoded, 0x20000, 2200)
|
|
|> add_job_if(encoded, 0x100000, 2000)
|
|
|> add_job_if(encoded, 0x100000, 2001)
|
|
|> add_job_if(encoded, 0x200000, 2100)
|
|
|> add_job_if(encoded, 0x400000, 2200)
|
|
|> add_job_if(encoded, 0x40000000, 3000)
|
|
|> add_job_if(encoded, 0x40000000, 3200)
|
|
|> add_job_if(encoded, 0x40000000, 3300)
|
|
|> add_job_if(encoded, 0x40000000, 3500)
|
|
|> Enum.uniq()
|
|
end
|
|
|
|
@doc "Gets job list from simple encoding"
|
|
@spec get_job_by_simple_encoding(integer()) :: [integer()]
|
|
def get_job_by_simple_encoding(encoded) do
|
|
[]
|
|
|> add_job_if(encoded, 0x1, 200)
|
|
|> add_job_if(encoded, 0x2, 300)
|
|
|> add_job_if(encoded, 0x4, 400)
|
|
|> add_job_if(encoded, 0x8, 500)
|
|
end
|
|
|
|
## Private Functions
|
|
|
|
defp add_job_if(list, encoded, flag, job) do
|
|
if Bitwise.band(encoded, flag) != 0 do
|
|
[job | list]
|
|
else
|
|
list
|
|
end
|
|
end
|
|
|
|
defp parse_action_data(:exp, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(:money, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(:pop, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(:sp, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
|
|
applicable_jobs =
|
|
map
|
|
|> Map.get(:applicable_jobs, Map.get(map, "applicable_jobs", []))
|
|
|> parse_job_list()
|
|
|
|
{int_store, applicable_jobs, int_store}
|
|
end
|
|
|
|
defp parse_action_data(:item, map) do
|
|
items =
|
|
map
|
|
|> Map.get(:value, Map.get(map, "value", []))
|
|
|> parse_item_list()
|
|
|
|
{items, [], 0}
|
|
end
|
|
|
|
defp parse_action_data(:skill, map) do
|
|
skills =
|
|
map
|
|
|> Map.get(:value, Map.get(map, "value", []))
|
|
|> parse_skill_list()
|
|
|
|
applicable_jobs =
|
|
map
|
|
|> Map.get(:applicable_jobs, Map.get(map, "applicable_jobs", []))
|
|
|> parse_job_list()
|
|
|
|
{skills, applicable_jobs, 0}
|
|
end
|
|
|
|
defp parse_action_data(:quest, map) do
|
|
quests =
|
|
map
|
|
|> Map.get(:value, Map.get(map, "value", []))
|
|
|> parse_quest_state_list()
|
|
|
|
{quests, [], 0}
|
|
end
|
|
|
|
defp parse_action_data(:nextQuest, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(:buffItemID, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(:infoNumber, map) do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
# Trait EXP actions
|
|
defp parse_action_data(type, map)
|
|
when type in [:charmEXP, :charismaEXP, :craftEXP, :insightEXP, :senseEXP, :willEXP] do
|
|
int_store = Map.get(map, :value, Map.get(map, "value", Map.get(map, :int_store, 0)))
|
|
{int_store, [], int_store}
|
|
end
|
|
|
|
defp parse_action_data(_type, map) do
|
|
{Map.get(map, :value, nil), [], 0}
|
|
end
|
|
|
|
defp parse_job_list(nil), do: []
|
|
defp parse_job_list(list) when is_list(list), do: list
|
|
defp parse_job_list(map) when is_map(map), do: Map.values(map)
|
|
|
|
defp parse_item_list(items) when is_list(items) do
|
|
Enum.map(items, fn item_data ->
|
|
%QuestItem{
|
|
item_id: Map.get(item_data, :id, Map.get(item_data, "id", Map.get(item_data, :item_id, 0))),
|
|
count: Map.get(item_data, :count, Map.get(item_data, "count", 1)),
|
|
period: Map.get(item_data, :period, Map.get(item_data, "period", 0)),
|
|
gender: Map.get(item_data, :gender, Map.get(item_data, "gender", 2)),
|
|
job: Map.get(item_data, :job, Map.get(item_data, "job", -1)),
|
|
job_ex: Map.get(item_data, :jobEx, Map.get(item_data, :job_ex, Map.get(item_data, "jobEx", -1))),
|
|
prop: Map.get(item_data, :prop, Map.get(item_data, "prop", -2))
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp parse_item_list(_), do: []
|
|
|
|
defp parse_skill_list(skills) when is_list(skills) do
|
|
Enum.map(skills, fn skill_data ->
|
|
%{
|
|
skill_id: Map.get(skill_data, :id, Map.get(skill_data, "id", 0)),
|
|
skill_level: Map.get(skill_data, :skill_level, Map.get(skill_data, "skillLevel", 0)),
|
|
master_level: Map.get(skill_data, :master_level, Map.get(skill_data, "masterLevel", 0))
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp parse_skill_list(_), do: []
|
|
|
|
defp parse_quest_state_list(quests) when is_list(quests) do
|
|
Enum.map(quests, fn quest_data ->
|
|
{
|
|
Map.get(quest_data, :id, Map.get(quest_data, "id", 0)),
|
|
Map.get(quest_data, :state, Map.get(quest_data, "state", 0))
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp parse_quest_state_list(quests) when is_map(quests) do
|
|
Enum.map(quests, fn {id, state} ->
|
|
{String.to_integer(id), state}
|
|
end)
|
|
end
|
|
|
|
defp parse_quest_state_list(_), do: []
|
|
|
|
# Start action implementations
|
|
|
|
defp do_run_start(:exp, %{int_store: exp} = _action, character) do
|
|
# Apply EXP with quest rate multiplier
|
|
# Full implementation would check GameConstants.getExpRate_Quest and trait bonuses
|
|
apply_exp(character, exp)
|
|
end
|
|
|
|
defp do_run_start(:money, %{int_store: meso} = _action, character) do
|
|
current_meso = Map.get(character, :meso, 0)
|
|
Map.put(character, :meso, current_meso + meso)
|
|
end
|
|
|
|
defp do_run_start(:pop, %{int_store: fame} = _action, character) do
|
|
current_fame = Map.get(character, :fame, 0)
|
|
Map.put(character, :fame, current_fame + fame)
|
|
end
|
|
|
|
defp do_run_start(:item, %{value: items} = action, character) do
|
|
# Filter items by job/gender restrictions
|
|
applicable_items =
|
|
items
|
|
|> Enum.filter(fn item -> can_get_item?(item, character) end)
|
|
|> select_items_by_prop()
|
|
|
|
# Add items to inventory (simplified - full implementation needs inventory manipulation)
|
|
Enum.reduce(applicable_items, character, fn item, char ->
|
|
add_item_to_character(char, item)
|
|
end)
|
|
end
|
|
|
|
defp do_run_start(:sp, %{int_store: sp, applicable_jobs: jobs} = _action, character) do
|
|
apply_skill_points(character, sp, jobs)
|
|
end
|
|
|
|
defp do_run_start(:skill, %{value: skills, applicable_jobs: jobs} = _action, character) do
|
|
apply_skills(character, skills, jobs)
|
|
end
|
|
|
|
defp do_run_start(:quest, %{value: quest_states} = _action, character) do
|
|
Enum.reduce(quest_states, character, fn {quest_id, state}, char ->
|
|
update_quest_state(char, quest_id, state)
|
|
end)
|
|
end
|
|
|
|
defp do_run_start(:nextQuest, %{int_store: next_quest_id} = _action, character) do
|
|
# Queue next quest
|
|
current_next = Map.get(character, :next_quest, nil)
|
|
|
|
if current_next == nil do
|
|
Map.put(character, :next_quest, next_quest_id)
|
|
else
|
|
character
|
|
end
|
|
end
|
|
|
|
# Trait EXP start actions
|
|
defp do_run_start(type, %{int_store: exp} = _action, character)
|
|
when type in [:charmEXP, :charismaEXP, :craftEXP, :insightEXP, :senseEXP, :willEXP] do
|
|
trait_name =
|
|
case type do
|
|
:charmEXP -> :charm
|
|
:charismaEXP -> :charisma
|
|
:craftEXP -> :craft
|
|
:insightEXP -> :insight
|
|
:senseEXP -> :sense
|
|
:willEXP -> :will
|
|
end
|
|
|
|
apply_trait_exp(character, trait_name, exp)
|
|
end
|
|
|
|
defp do_run_start(_type, _action, character), do: character
|
|
|
|
# End action implementations (mostly same as start but without forfeiture check)
|
|
|
|
defp do_run_end(:exp, %{int_store: exp} = _action, character) do
|
|
apply_exp(character, exp)
|
|
end
|
|
|
|
defp do_run_end(:money, %{int_store: meso} = _action, character) do
|
|
current_meso = Map.get(character, :meso, 0)
|
|
Map.put(character, :meso, current_meso + meso)
|
|
end
|
|
|
|
defp do_run_end(:pop, %{int_store: fame} = _action, character) do
|
|
current_fame = Map.get(character, :fame, 0)
|
|
Map.put(character, :fame, current_fame + fame)
|
|
end
|
|
|
|
defp do_run_end(:item, %{value: items} = action, character) do
|
|
applicable_items =
|
|
items
|
|
|> Enum.filter(fn item -> can_get_item?(item, character) end)
|
|
|> select_items_by_prop()
|
|
|
|
Enum.reduce(applicable_items, character, fn item, char ->
|
|
add_item_to_character(char, item)
|
|
end)
|
|
end
|
|
|
|
defp do_run_end(:sp, %{int_store: sp, applicable_jobs: jobs} = _action, character) do
|
|
apply_skill_points(character, sp, jobs)
|
|
end
|
|
|
|
defp do_run_end(:skill, %{value: skills, applicable_jobs: jobs} = _action, character) do
|
|
apply_skills(character, skills, jobs)
|
|
end
|
|
|
|
defp do_run_end(:quest, %{value: quest_states} = _action, character) do
|
|
Enum.reduce(quest_states, character, fn {quest_id, state}, char ->
|
|
update_quest_state(char, quest_id, state)
|
|
end)
|
|
end
|
|
|
|
defp do_run_end(:nextQuest, %{int_store: next_quest_id} = _action, character) do
|
|
current_next = Map.get(character, :next_quest, nil)
|
|
|
|
if current_next == nil do
|
|
Map.put(character, :next_quest, next_quest_id)
|
|
else
|
|
character
|
|
end
|
|
end
|
|
|
|
defp do_run_end(:buffItemID, %{int_store: item_id} = _action, character) when item_id > 0 do
|
|
# Apply item buff effect
|
|
# Full implementation would get item effect from ItemInformationProvider
|
|
character
|
|
end
|
|
|
|
# Trait EXP end actions
|
|
defp do_run_end(type, %{int_store: exp} = _action, character)
|
|
when type in [:charmEXP, :charismaEXP, :craftEXP, :insightEXP, :senseEXP, :willEXP] do
|
|
trait_name =
|
|
case type do
|
|
:charmEXP -> :charm
|
|
:charismaEXP -> :charisma
|
|
:craftEXP -> :craft
|
|
:insightEXP -> :insight
|
|
:senseEXP -> :sense
|
|
:willEXP -> :will
|
|
end
|
|
|
|
apply_trait_exp(character, trait_name, exp)
|
|
end
|
|
|
|
defp do_run_end(_type, _action, character), do: character
|
|
|
|
# Check end implementations
|
|
|
|
defp do_check_end(:item, %{value: items} = action, character) do
|
|
# Check if character has inventory space for items
|
|
applicable_items =
|
|
items
|
|
|> Enum.filter(fn item -> can_get_item?(item, character) end)
|
|
|> select_items_by_prop()
|
|
|
|
# Count items by inventory type
|
|
needed_slots =
|
|
Enum.reduce(applicable_items, %{equip: 0, use: 0, setup: 0, etc: 0, cash: 0}, fn item, acc ->
|
|
inv_type = get_inventory_type(item.item_id)
|
|
Map.update(acc, inv_type, 1, &(&1 + 1))
|
|
end)
|
|
|
|
# Check available space (simplified)
|
|
inventory = Map.get(character, :inventory, %{})
|
|
|
|
Enum.all?(needed_slots, fn {type, count} ->
|
|
current_items = Map.get(inventory, type, [])
|
|
max_slots = get_max_slots(type)
|
|
length(current_items) + count <= max_slots
|
|
end)
|
|
end
|
|
|
|
defp do_check_end(:money, %{int_store: meso} = _action, character) do
|
|
current_meso = Map.get(character, :meso, 0)
|
|
|
|
cond do
|
|
meso > 0 and current_meso + meso > 2_147_483_647 ->
|
|
# Would overflow
|
|
false
|
|
|
|
meso < 0 and current_meso < abs(meso) ->
|
|
# Not enough meso
|
|
false
|
|
|
|
true ->
|
|
true
|
|
end
|
|
end
|
|
|
|
defp do_check_end(_type, _action, _character), do: true
|
|
|
|
# Helper functions
|
|
|
|
defp select_items_by_prop(items) do
|
|
# Handle probability-based item selection
|
|
# Items with prop > 0 are selected randomly
|
|
# Items with prop == -1 are selection-based (user chooses)
|
|
# Items with prop == -2 are always given
|
|
|
|
{random_items, other_items} =
|
|
Enum.split_with(items, fn item -> item.prop > 0 end)
|
|
|
|
if length(random_items) > 0 do
|
|
# Create weighted pool
|
|
pool =
|
|
Enum.flat_map(random_items, fn item ->
|
|
List.duplicate(item, item.prop)
|
|
end)
|
|
|
|
selected = Enum.random(pool)
|
|
[selected | other_items]
|
|
else
|
|
other_items
|
|
end
|
|
end
|
|
|
|
defp add_item_to_character(character, %QuestItem{} = item) do
|
|
inventory = Map.get(character, :inventory, %{})
|
|
inv_type = get_inventory_type(item.item_id)
|
|
|
|
new_item = %{
|
|
item_id: item.item_id,
|
|
quantity: item.count,
|
|
position: find_next_slot(inventory, inv_type),
|
|
expiration: if(item.period > 0, do: System.system_time(:second) + item.period * 60, else: -1)
|
|
}
|
|
|
|
updated_inventory =
|
|
Map.update(inventory, inv_type, [new_item], fn items ->
|
|
[new_item | items]
|
|
end)
|
|
|
|
Map.put(character, :inventory, updated_inventory)
|
|
end
|
|
|
|
defp get_inventory_type(item_id) do
|
|
prefix = div(item_id, 1_000_000)
|
|
|
|
case prefix do
|
|
1 -> :equip
|
|
2 -> :use
|
|
3 -> :setup
|
|
4 -> :etc
|
|
5 -> :cash
|
|
_ -> :etc
|
|
end
|
|
end
|
|
|
|
defp get_max_slots(type) do
|
|
case type do
|
|
:equip -> 24
|
|
:use -> 80
|
|
:setup -> 80
|
|
:etc -> 80
|
|
:cash -> 40
|
|
_ -> 80
|
|
end
|
|
end
|
|
|
|
defp find_next_slot(inventory, type) do
|
|
items = Map.get(inventory, type, [])
|
|
positions = Enum.map(items, & &1.position)
|
|
|
|
Enum.find(1..100, fn slot ->
|
|
slot not in positions
|
|
end) || 0
|
|
end
|
|
|
|
defp apply_exp(character, base_exp) do
|
|
level = Map.get(character, :level, 1)
|
|
|
|
# Apply quest EXP rate
|
|
exp_rate = 1.0 # Would get from GameConstants
|
|
|
|
# Apply trait bonus (Sense trait gives quest EXP bonus)
|
|
traits = Map.get(character, :traits, %{})
|
|
sense_level = Map.get(traits, :sense, 0)
|
|
trait_bonus = 1.0 + (sense_level * 3 / 1000)
|
|
|
|
final_exp = trunc(base_exp * exp_rate * trait_bonus)
|
|
|
|
# Add EXP to character
|
|
current_exp = Map.get(character, :exp, 0)
|
|
Map.put(character, :exp, current_exp + final_exp)
|
|
end
|
|
|
|
defp apply_skill_points(character, sp, jobs) do
|
|
character_job = Map.get(character, :job, 0)
|
|
|
|
# Find most applicable job
|
|
applicable_job =
|
|
jobs
|
|
|> Enum.filter(fn job -> character_job >= job end)
|
|
|> Enum.max(fn -> 0 end)
|
|
|
|
sp_type =
|
|
if applicable_job == 0 do
|
|
# Beginner SP
|
|
0
|
|
else
|
|
# Get skill book based on job
|
|
get_skill_book(applicable_job)
|
|
end
|
|
|
|
current_sp = Map.get(character, :sp, [])
|
|
updated_sp = List.replace_at(current_sp, sp_type, (Enum.at(current_sp, sp_type, 0) || 0) + sp)
|
|
|
|
Map.put(character, :sp, updated_sp)
|
|
end
|
|
|
|
defp get_skill_book(job) do
|
|
# Get skill book index for job
|
|
cond do
|
|
job >= 1000 and job < 2000 -> 1
|
|
job >= 2000 and job < 3000 -> 2
|
|
job >= 3000 and job < 4000 -> 3
|
|
job >= 4000 and job < 5000 -> 4
|
|
true -> 0
|
|
end
|
|
end
|
|
|
|
defp apply_skills(character, skills, applicable_jobs) do
|
|
character_job = Map.get(character, :job, 0)
|
|
|
|
# Check if any job matches
|
|
job_matches = Enum.any?(applicable_jobs, fn job -> character_job == job end)
|
|
|
|
if job_matches or applicable_jobs == [] do
|
|
current_skills = Map.get(character, :skills, %{})
|
|
current_master_levels = Map.get(character, :skill_master_levels, %{})
|
|
|
|
Enum.reduce(skills, character, fn skill, char ->
|
|
skill_id = skill.skill_id
|
|
skill_level = skill.skill_level
|
|
master_level = skill.master_level
|
|
|
|
# Get current levels
|
|
current_level = Map.get(current_skills, skill_id, 0)
|
|
current_master = Map.get(current_master_levels, skill_id, 0)
|
|
|
|
# Update with max of current/new
|
|
new_skills = Map.put(current_skills, skill_id, max(skill_level, current_level))
|
|
new_masters = Map.put(current_master_levels, skill_id, max(master_level, current_master))
|
|
|
|
char
|
|
|> Map.put(:skills, new_skills)
|
|
|> Map.put(:skill_master_levels, new_masters)
|
|
end)
|
|
else
|
|
character
|
|
end
|
|
end
|
|
|
|
defp update_quest_state(character, quest_id, state) do
|
|
quest_progress = Map.get(character, :quest_progress, %{})
|
|
updated_progress = Map.put(quest_progress, quest_id, state)
|
|
Map.put(character, :quest_progress, updated_progress)
|
|
end
|
|
|
|
defp apply_trait_exp(character, trait_name, exp) do
|
|
traits = Map.get(character, :traits, %{})
|
|
current_exp = Map.get(traits, trait_name, 0)
|
|
updated_traits = Map.put(traits, trait_name, current_exp + exp)
|
|
Map.put(character, :traits, updated_traits)
|
|
end
|
|
end
|