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