port over some more
This commit is contained in:
@@ -94,6 +94,10 @@ defmodule Odinsea.Game.Character do
|
||||
:face,
|
||||
# GM Level (0 = normal player, >0 = GM)
|
||||
:gm,
|
||||
# Guild
|
||||
:guild_id,
|
||||
:guild_rank,
|
||||
:alliance_rank,
|
||||
# Stats
|
||||
:stats,
|
||||
# Position & Map
|
||||
@@ -134,6 +138,9 @@ defmodule Odinsea.Game.Character do
|
||||
hair: non_neg_integer(),
|
||||
face: non_neg_integer(),
|
||||
gm: non_neg_integer(),
|
||||
guild_id: non_neg_integer(),
|
||||
guild_rank: non_neg_integer(),
|
||||
alliance_rank: non_neg_integer(),
|
||||
stats: Stats.t(),
|
||||
map_id: non_neg_integer(),
|
||||
position: Position.t(),
|
||||
@@ -281,6 +288,30 @@ defmodule Odinsea.Game.Character do
|
||||
GenServer.call(via_tuple(character_id), {:drop_item, inv_type, position, quantity})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds meso to the character.
|
||||
Returns {:ok, new_meso} on success, {:error, reason} on failure.
|
||||
"""
|
||||
def gain_meso(character_id, amount, show_in_chat \\ false) do
|
||||
GenServer.call(via_tuple(character_id), {:gain_meso, amount, show_in_chat})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the character has inventory space for an item.
|
||||
Returns {:ok, slot} with the next free slot, or {:error, :inventory_full}.
|
||||
"""
|
||||
def check_inventory_space(character_id, inv_type, quantity \\ 1) do
|
||||
GenServer.call(via_tuple(character_id), {:check_inventory_space, inv_type, quantity})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to the character's inventory (from a drop).
|
||||
Returns {:ok, item} on success, {:error, reason} on failure.
|
||||
"""
|
||||
def add_item_from_drop(character_id, item) do
|
||||
GenServer.call(via_tuple(character_id), {:add_item_from_drop, item})
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# GenServer Callbacks
|
||||
# ============================================================================
|
||||
@@ -423,6 +454,45 @@ defmodule Odinsea.Game.Character do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:gain_meso, amount, show_in_chat}, _from, state) do
|
||||
# Cap meso at 9,999,999,999 (MapleStory max)
|
||||
max_meso = 9_999_999_999
|
||||
new_meso = min(state.meso + amount, max_meso)
|
||||
|
||||
new_state = %{state | meso: new_meso, updated_at: DateTime.utc_now()}
|
||||
|
||||
# TODO: Send meso gain packet to client if show_in_chat is true
|
||||
|
||||
{:reply, {:ok, new_meso}, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:check_inventory_space, inv_type, _quantity}, _from, state) do
|
||||
inventory = Map.get(state.inventories, inv_type, Inventory.new(inv_type))
|
||||
|
||||
case Inventory.get_next_free_slot(inventory) do
|
||||
nil -> {:reply, {:error, :inventory_full}, state}
|
||||
slot -> {:reply, {:ok, slot}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:add_item_from_drop, item}, _from, state) do
|
||||
inv_type = get_inventory_type_from_item_id(item.item_id)
|
||||
inventory = Map.get(state.inventories, inv_type, Inventory.new(inv_type))
|
||||
|
||||
case Inventory.add_item(inventory, item) do
|
||||
{:ok, new_inventory, assigned_item} ->
|
||||
new_inventories = Map.put(state.inventories, inv_type, new_inventory)
|
||||
new_state = %{state | inventories: new_inventories, updated_at: DateTime.utc_now()}
|
||||
{:reply, {:ok, assigned_item}, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:update_position, position}, state) do
|
||||
new_state = %{
|
||||
@@ -459,6 +529,19 @@ defmodule Odinsea.Game.Character do
|
||||
{:via, Registry, {Odinsea.CharacterRegistry, character_id}}
|
||||
end
|
||||
|
||||
defp get_inventory_type_from_item_id(item_id) do
|
||||
type_prefix = div(item_id, 1_000_000)
|
||||
|
||||
case type_prefix do
|
||||
1 -> :equip
|
||||
2 -> :use
|
||||
3 -> :setup
|
||||
4 -> :etc
|
||||
5 -> :cash
|
||||
_ -> :etc
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts database character to in-game state.
|
||||
"""
|
||||
@@ -517,6 +600,9 @@ defmodule Odinsea.Game.Character do
|
||||
hair: db_char.hair,
|
||||
face: db_char.face,
|
||||
gm: db_char.gm,
|
||||
guild_id: db_char.guild_id || 0,
|
||||
guild_rank: db_char.guild_rank || 0,
|
||||
alliance_rank: db_char.alliance_rank || 0,
|
||||
stats: stats,
|
||||
map_id: db_char.map_id,
|
||||
position: position,
|
||||
@@ -850,4 +936,112 @@ defmodule Odinsea.Game.Character do
|
||||
# TODO: Use actual MapleStory EXP table
|
||||
level * level * level + 100 * level
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Scripting API Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Gets the character's channel ID.
|
||||
"""
|
||||
def get_channel(character_id) do
|
||||
case get_state(character_id) do
|
||||
nil -> {:error, :character_not_found}
|
||||
%State{channel_id: channel_id} -> {:ok, channel_id}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the character's meso.
|
||||
"""
|
||||
def update_meso(character_id, new_meso) do
|
||||
GenServer.cast(via_tuple(character_id), {:update_meso, new_meso})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the character's job.
|
||||
"""
|
||||
def update_job(character_id, new_job) do
|
||||
GenServer.cast(via_tuple(character_id), {:update_job, new_job})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a skill level.
|
||||
"""
|
||||
def update_skill(character_id, skill_id, level, master_level) do
|
||||
GenServer.cast(via_tuple(character_id), {:update_skill, skill_id, level, master_level})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to inventory.
|
||||
"""
|
||||
def add_item(character_id, inventory_type, item) do
|
||||
GenServer.call(via_tuple(character_id), {:add_item, inventory_type, item})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes items by item ID.
|
||||
"""
|
||||
def remove_item_by_id(character_id, item_id, quantity) do
|
||||
GenServer.call(via_tuple(character_id), {:remove_item_by_id, item_id, quantity})
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# GenServer Callbacks - Scripting Operations
|
||||
# ============================================================================
|
||||
|
||||
@impl true
|
||||
def handle_cast({:update_meso, new_meso}, state) do
|
||||
new_state = %{state | meso: new_meso, updated_at: DateTime.utc_now()}
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:update_job, new_job}, state) do
|
||||
new_state = %{state | job: new_job, updated_at: DateTime.utc_now()}
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:update_skill, skill_id, level, master_level}, state) do
|
||||
skill_entry = %{
|
||||
level: level,
|
||||
master_level: master_level,
|
||||
expiration: -1
|
||||
}
|
||||
new_skills = Map.put(state.skills, skill_id, skill_entry)
|
||||
new_state = %{state | skills: new_skills, updated_at: DateTime.utc_now()}
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:add_item, inventory_type, item}, _from, state) do
|
||||
inventory = Map.get(state.inventories, inventory_type, Inventory.new(inventory_type))
|
||||
|
||||
case Inventory.add_item(inventory, item) do
|
||||
{:ok, new_inventory} ->
|
||||
new_inventories = Map.put(state.inventories, inventory_type, new_inventory)
|
||||
new_state = %{state | inventories: new_inventories, updated_at: DateTime.utc_now()}
|
||||
{:reply, :ok, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:remove_item_by_id, item_id, quantity}, _from, state) do
|
||||
inventory_type = Inventory.get_type_by_item_id(item_id)
|
||||
inventory = Map.get(state.inventories, inventory_type, Inventory.new(inventory_type))
|
||||
|
||||
case Inventory.remove_by_id(inventory, item_id, quantity) do
|
||||
{:ok, new_inventory, _removed} ->
|
||||
new_inventories = Map.put(state.inventories, inventory_type, new_inventory)
|
||||
new_state = %{state | inventories: new_inventories, updated_at: DateTime.utc_now()}
|
||||
{:reply, :ok, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,6 +14,8 @@ defmodule Odinsea.Game.Drop do
|
||||
- Type 3: Explosive/FFA (instant FFA)
|
||||
"""
|
||||
|
||||
alias Odinsea.Game.Item
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
oid: integer(),
|
||||
item_id: integer(),
|
||||
@@ -30,7 +32,9 @@ defmodule Odinsea.Game.Drop do
|
||||
created_at: integer(),
|
||||
expire_time: integer() | nil,
|
||||
public_time: integer() | nil,
|
||||
dropper_oid: integer() | nil
|
||||
dropper_oid: integer() | nil,
|
||||
# Item struct for item drops (nil for meso drops)
|
||||
item: Item.t() | nil
|
||||
}
|
||||
|
||||
defstruct [
|
||||
@@ -49,7 +53,8 @@ defmodule Odinsea.Game.Drop do
|
||||
:created_at,
|
||||
:expire_time,
|
||||
:public_time,
|
||||
:dropper_oid
|
||||
:dropper_oid,
|
||||
:item
|
||||
]
|
||||
|
||||
# Default drop expiration times (milliseconds)
|
||||
@@ -65,6 +70,7 @@ defmodule Odinsea.Game.Drop do
|
||||
individual_reward = Keyword.get(opts, :individual_reward, false)
|
||||
dropper_oid = Keyword.get(opts, :dropper_oid, nil)
|
||||
source_position = Keyword.get(opts, :source_position, nil)
|
||||
item = Keyword.get(opts, :item, nil)
|
||||
now = System.system_time(:millisecond)
|
||||
|
||||
%__MODULE__{
|
||||
@@ -83,7 +89,8 @@ defmodule Odinsea.Game.Drop do
|
||||
created_at: now,
|
||||
expire_time: now + @default_expire_time,
|
||||
public_time: if(drop_type < 2, do: now + @default_public_time, else: 0),
|
||||
dropper_oid: dropper_oid
|
||||
dropper_oid: dropper_oid,
|
||||
item: item
|
||||
}
|
||||
end
|
||||
|
||||
@@ -113,7 +120,8 @@ defmodule Odinsea.Game.Drop do
|
||||
created_at: now,
|
||||
expire_time: now + @default_expire_time,
|
||||
public_time: if(drop_type < 2, do: now + @default_public_time, else: 0),
|
||||
dropper_oid: dropper_oid
|
||||
dropper_oid: dropper_oid,
|
||||
item: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -141,13 +149,22 @@ defmodule Odinsea.Game.Drop do
|
||||
@doc """
|
||||
Checks if a drop is visible to a specific character.
|
||||
Considers quest requirements and individual rewards.
|
||||
|
||||
For quest items, the character must have the quest started.
|
||||
For individual rewards, only the owner can see the drop.
|
||||
"""
|
||||
def visible_to?(%__MODULE__{} = drop, character_id, _quest_status) do
|
||||
def visible_to?(%__MODULE__{} = drop, character_id, quest_status) do
|
||||
# Individual rewards only visible to owner
|
||||
if drop.individual_reward do
|
||||
drop.owner_id == character_id
|
||||
else
|
||||
true
|
||||
# Check quest requirement
|
||||
if drop.quest_id > 0 do
|
||||
# Only visible if character has quest started (status 1)
|
||||
Map.get(quest_status, drop.quest_id, 0) == 1
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -158,6 +175,13 @@ defmodule Odinsea.Game.Drop do
|
||||
drop.meso > 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if this is an item drop.
|
||||
"""
|
||||
def item?(%__MODULE__{} = drop) do
|
||||
drop.meso == 0 and drop.item_id > 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the display ID (item_id for items, meso amount for meso).
|
||||
"""
|
||||
@@ -170,7 +194,13 @@ defmodule Odinsea.Game.Drop do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a character can loot this drop.
|
||||
Checks if a character can loot this drop based on ownership rules.
|
||||
|
||||
Drop types:
|
||||
- 0: Owner only (until timeout)
|
||||
- 1: Owner's party (until timeout)
|
||||
- 2: Free-for-all (FFA)
|
||||
- 3: Explosive (instant FFA)
|
||||
"""
|
||||
def can_loot?(%__MODULE__{} = drop, character_id, now) do
|
||||
# If already picked up, can't loot
|
||||
@@ -197,4 +227,35 @@ defmodule Odinsea.Game.Drop do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a character can loot this drop, including party check.
|
||||
Requires party information to validate party drops.
|
||||
"""
|
||||
def can_loot_with_party?(%__MODULE__{} = drop, character_id, party_id, party_members, now) do
|
||||
if drop.picked_up do
|
||||
false
|
||||
else
|
||||
case drop.drop_type do
|
||||
0 ->
|
||||
# Timeout for non-owner only
|
||||
drop.owner_id == character_id or is_public_time?(drop, now)
|
||||
1 ->
|
||||
# Timeout for non-owner's party
|
||||
owner_in_same_party = drop.owner_id in party_members
|
||||
(owner_in_same_party and party_id != nil) or
|
||||
drop.owner_id == character_id or
|
||||
is_public_time?(drop, now)
|
||||
2 ->
|
||||
# FFA
|
||||
true
|
||||
3 ->
|
||||
# Explosive/FFA (instant FFA)
|
||||
true
|
||||
_ ->
|
||||
# Default to owner-only
|
||||
drop.owner_id == character_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -167,6 +167,17 @@ defmodule Odinsea.Game.Inventory do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the next available slot number.
|
||||
Returns nil if the inventory is full (Elixir-style).
|
||||
"""
|
||||
def get_next_free_slot(%__MODULE__{} = inv) do
|
||||
case next_free_slot(inv) do
|
||||
-1 -> nil
|
||||
slot -> slot
|
||||
end
|
||||
end
|
||||
|
||||
defp find_next_slot(items, limit, slot) when slot > limit, do: -1
|
||||
|
||||
defp find_next_slot(items, limit, slot) do
|
||||
@@ -205,6 +216,23 @@ defmodule Odinsea.Game.Inventory do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a plain map item to the inventory (used for drops).
|
||||
Returns {:ok, new_inventory, assigned_item} or {:error, :inventory_full}.
|
||||
"""
|
||||
def add_item(%__MODULE__{} = inv, %{} = item_map) when not is_struct(item_map) do
|
||||
slot = next_free_slot(inv)
|
||||
|
||||
if slot < 0 do
|
||||
{:error, :inventory_full}
|
||||
else
|
||||
# Convert map to item with assigned position
|
||||
assigned_item = Map.put(item_map, :position, slot)
|
||||
new_items = Map.put(inv.items, slot, assigned_item)
|
||||
{:ok, %{inv | items: new_items}, assigned_item}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item from the database (preserves position).
|
||||
"""
|
||||
@@ -391,4 +419,103 @@ defmodule Odinsea.Game.Inventory do
|
||||
end
|
||||
|
||||
def equipped_items(%__MODULE__{}), do: []
|
||||
|
||||
@doc """
|
||||
Gets the inventory type based on item ID.
|
||||
"""
|
||||
def get_type_by_item_id(item_id) do
|
||||
InventoryType.from_item_id(item_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if inventory has at least the specified quantity of an item.
|
||||
"""
|
||||
def has_item_count(%__MODULE__{} = inv, item_id, quantity) do
|
||||
count_by_id(inv, item_id) >= quantity
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if there's at least one free slot in the inventory.
|
||||
"""
|
||||
def has_free_slot(%__MODULE__{} = inv) do
|
||||
next_free_slot(inv) >= 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if inventory can hold the specified quantity of an item.
|
||||
For stackable items, checks if there's room to stack or a free slot.
|
||||
"""
|
||||
def can_hold_quantity(%__MODULE__{} = inv, item_id, quantity) do
|
||||
# Find existing item to check stack space
|
||||
existing = find_by_id(inv, item_id)
|
||||
slot_max = InventoryType.slot_limit(inv.type)
|
||||
|
||||
if existing do
|
||||
# Check if we can stack
|
||||
space_in_stack = slot_max - existing.quantity
|
||||
remaining = quantity - space_in_stack
|
||||
|
||||
if remaining <= 0 do
|
||||
true
|
||||
else
|
||||
# Need additional slots
|
||||
free_slots = count_free_slots(inv)
|
||||
slots_needed = div(remaining, slot_max) + if rem(remaining, slot_max) > 0, do: 1, else: 0
|
||||
free_slots >= slots_needed
|
||||
end
|
||||
else
|
||||
# Need new slot(s)
|
||||
free_slots = count_free_slots(inv)
|
||||
slots_needed = div(quantity, slot_max) + if rem(quantity, slot_max) > 0, do: 1, else: 0
|
||||
free_slots >= slots_needed
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes items by item ID.
|
||||
Returns {:ok, new_inventory, removed_count} or {:error, reason}.
|
||||
"""
|
||||
def remove_by_id(%__MODULE__{} = inv, item_id, quantity) do
|
||||
items_with_id =
|
||||
inv.items
|
||||
|> Map.values()
|
||||
|> Enum.filter(fn item -> item.item_id == item_id end)
|
||||
|> Enum.sort_by(fn item -> item.position end)
|
||||
|
||||
total_available = Enum.map(items_with_id, fn i -> i.quantity end) |> Enum.sum()
|
||||
|
||||
if total_available < quantity do
|
||||
{:error, :insufficient_quantity}
|
||||
else
|
||||
{new_items, removed} = do_remove_by_id(inv.items, items_with_id, quantity, 0)
|
||||
{:ok, %{inv | items: new_items}, removed}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remove_by_id(items, _items_to_remove, 0, removed), do: {items, removed}
|
||||
defp do_remove_by_id(items, [], _quantity, removed), do: {items, removed}
|
||||
defp do_remove_by_id(items, [item | rest], quantity, removed) do
|
||||
if quantity <= 0 do
|
||||
{items, removed}
|
||||
else
|
||||
to_remove = min(item.quantity, quantity)
|
||||
new_quantity = item.quantity - to_remove
|
||||
|
||||
new_items = if new_quantity <= 0 do
|
||||
Map.delete(items, item.position)
|
||||
else
|
||||
Map.put(items, item.position, %{item | quantity: new_quantity})
|
||||
end
|
||||
|
||||
do_remove_by_id(new_items, rest, quantity - to_remove, removed + to_remove)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts free slots in the inventory.
|
||||
"""
|
||||
def count_free_slots(%__MODULE__{} = inv) do
|
||||
used_slots = map_size(inv.items)
|
||||
inv.slot_limit - used_slots
|
||||
end
|
||||
end
|
||||
|
||||
120
lib/odinsea/game/inventory_manipulator.ex
Normal file
120
lib/odinsea/game/inventory_manipulator.ex
Normal file
@@ -0,0 +1,120 @@
|
||||
defmodule Odinsea.Game.InventoryManipulator do
|
||||
@moduledoc """
|
||||
High-level inventory operations for adding/removing items.
|
||||
Ported from Java server.MapleInventoryManipulator
|
||||
|
||||
This module provides convenient functions for:
|
||||
- Adding items from drops
|
||||
- Adding items by ID
|
||||
- Removing items
|
||||
- Checking inventory space
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Game.Character
|
||||
|
||||
@doc """
|
||||
Adds an item to the character's inventory from a drop.
|
||||
Returns {:ok, item} on success, {:error, reason} on failure.
|
||||
|
||||
Ported from MapleInventoryManipulator.addFromDrop()
|
||||
"""
|
||||
def add_from_drop(character_pid, item) when is_pid(character_pid) do
|
||||
Character.add_item_from_drop(character_pid, item)
|
||||
end
|
||||
|
||||
def add_from_drop(character_id, item) when is_integer(character_id) do
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, character_id) do
|
||||
[{pid, _}] -> add_from_drop(pid, item)
|
||||
[] -> {:error, :character_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to the character's inventory by item ID and quantity.
|
||||
Creates a new item instance with default properties.
|
||||
|
||||
Ported from MapleInventoryManipulator.addById()
|
||||
"""
|
||||
def add_by_id(character_pid, item_id, quantity \\ 1, gm_log \\ "") do
|
||||
# Create a basic item
|
||||
item = %{
|
||||
item_id: item_id,
|
||||
quantity: quantity,
|
||||
owner: "",
|
||||
flag: 0,
|
||||
gm_log: gm_log
|
||||
}
|
||||
|
||||
add_from_drop(character_pid, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to the character's inventory with full item details.
|
||||
|
||||
Ported from MapleInventoryManipulator.addbyItem()
|
||||
"""
|
||||
def add_by_item(character_pid, item) do
|
||||
add_from_drop(character_pid, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes an item from a specific inventory slot.
|
||||
|
||||
Ported from MapleInventoryManipulator.removeFromSlot()
|
||||
"""
|
||||
def remove_from_slot(character_pid, inv_type, slot, quantity \\ 1, _from_drop \\ false, _wedding \\ false) do
|
||||
Character.drop_item(character_pid, inv_type, slot, quantity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes items by item ID from the inventory.
|
||||
|
||||
Ported from MapleInventoryManipulator.removeById()
|
||||
"""
|
||||
def remove_by_id(_character_pid, _inv_type, _item_id, _quantity, _delete \\ false, _wedding \\ false) do
|
||||
# TODO: Implement remove by ID (search for item, then remove)
|
||||
{:ok, 0}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the character has space for an item.
|
||||
|
||||
Ported from MapleInventoryManipulator.checkSpace()
|
||||
"""
|
||||
def check_space(character_pid, item_id, quantity \\ 1, _owner \\ "") do
|
||||
inv_type = get_inventory_type(item_id)
|
||||
|
||||
case Character.check_inventory_space(character_pid, inv_type, quantity) do
|
||||
{:ok, _slot} -> true
|
||||
{:error, _} -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the character's inventory is full.
|
||||
"""
|
||||
def inventory_full?(character_pid, inv_type) do
|
||||
case Character.check_inventory_space(character_pid, inv_type, 1) do
|
||||
{:ok, _} -> false
|
||||
{:error, :inventory_full} -> true
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the inventory type from an item ID.
|
||||
"""
|
||||
def get_inventory_type(item_id) do
|
||||
type_prefix = div(item_id, 1_000_000)
|
||||
|
||||
case type_prefix do
|
||||
1 -> :equip
|
||||
2 -> :use
|
||||
3 -> :setup
|
||||
4 -> :etc
|
||||
5 -> :cash
|
||||
_ -> :etc
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -95,4 +95,33 @@ defmodule Odinsea.Game.InventoryType do
|
||||
Lists all inventory types including equipped.
|
||||
"""
|
||||
def all_types, do: [:equip, :use, :setup, :etc, :cash, :equipped]
|
||||
|
||||
@doc """
|
||||
Gets the inventory type from an item ID.
|
||||
Based on MapleStory item ID ranges:
|
||||
- 1000000-1999999: Equip
|
||||
- 2000000-2999999: Use (consumables)
|
||||
- 3000000-3999999: Setup
|
||||
- 4000000-4999999: Etc
|
||||
- 5000000-5999999: Cash
|
||||
"""
|
||||
def from_item_id(item_id) when is_integer(item_id) do
|
||||
cond do
|
||||
item_id >= 1_000_000 and item_id < 2_000_000 -> :equip
|
||||
item_id >= 2_000_000 and item_id < 3_000_000 -> :use
|
||||
item_id >= 3_000_000 and item_id < 4_000_000 -> :setup
|
||||
item_id >= 4_000_000 and item_id < 5_000_000 -> :etc
|
||||
item_id >= 5_000_000 and item_id < 6_000_000 -> :cash
|
||||
true -> :etc
|
||||
end
|
||||
end
|
||||
|
||||
def from_item_id(_), do: :etc
|
||||
|
||||
@doc """
|
||||
Gets slot limit for an inventory type.
|
||||
"""
|
||||
def slot_limit(type) do
|
||||
default_slot_limit(type)
|
||||
end
|
||||
end
|
||||
|
||||
110
lib/odinsea/game/job_type.ex
Normal file
110
lib/odinsea/game/job_type.ex
Normal file
@@ -0,0 +1,110 @@
|
||||
defmodule Odinsea.Game.JobType do
|
||||
@moduledoc """
|
||||
Job type definitions for character creation.
|
||||
Ported from Java LoginInformationProvider.JobType
|
||||
|
||||
Job types:
|
||||
- 0 = Resistance
|
||||
- 1 = Adventurer
|
||||
- 2 = Cygnus
|
||||
- 3 = Aran
|
||||
- 4 = Evan
|
||||
"""
|
||||
|
||||
@type t :: :resistance | :adventurer | :cygnus | :aran | :evan | :ultimate_adventurer
|
||||
|
||||
@doc """
|
||||
Converts an integer job type to atom.
|
||||
"""
|
||||
@spec from_int(integer()) :: t()
|
||||
def from_int(0), do: :resistance
|
||||
def from_int(1), do: :adventurer
|
||||
def from_int(2), do: :cygnus
|
||||
def from_int(3), do: :aran
|
||||
def from_int(4), do: :evan
|
||||
def from_int(_), do: :adventurer
|
||||
|
||||
@doc """
|
||||
Converts a job type atom to integer.
|
||||
"""
|
||||
@spec to_int(t()) :: integer()
|
||||
def to_int(:resistance), do: 0
|
||||
def to_int(:adventurer), do: 1
|
||||
def to_int(:cygnus), do: 2
|
||||
def to_int(:aran), do: 3
|
||||
def to_int(:evan), do: 4
|
||||
def to_int(:ultimate_adventurer), do: 5
|
||||
def to_int(_), do: 1
|
||||
|
||||
@doc """
|
||||
Gets the base job ID for a job type.
|
||||
"""
|
||||
@spec get_job_id(t()) :: integer()
|
||||
def get_job_id(:resistance), do: 3000
|
||||
def get_job_id(:adventurer), do: 0
|
||||
def get_job_id(:cygnus), do: 1000
|
||||
def get_job_id(:aran), do: 2000
|
||||
def get_job_id(:evan), do: 2001
|
||||
def get_job_id(:ultimate_adventurer), do: 0
|
||||
def get_job_id(_), do: 0
|
||||
|
||||
@doc """
|
||||
Checks if a job type is valid for character creation.
|
||||
"""
|
||||
@spec valid?(integer() | t()) :: boolean()
|
||||
def valid?(type) when is_integer(type), do: type >= 0 and type <= 4
|
||||
def valid?(type) when is_atom(type), do: type in [:resistance, :adventurer, :cygnus, :aran, :evan]
|
||||
def valid?(_), do: false
|
||||
|
||||
@doc """
|
||||
Gets the tutorial map ID for a job type.
|
||||
"""
|
||||
@spec get_tutorial_map(t() | integer()) :: integer()
|
||||
def get_tutorial_map(:resistance), do: 931000000
|
||||
def get_tutorial_map(:adventurer), do: 0 # Maple Island (special handling)
|
||||
def get_tutorial_map(:cygnus), do: 130030000
|
||||
def get_tutorial_map(:aran), do: 914000000
|
||||
def get_tutorial_map(:evan), do: 900010000
|
||||
def get_tutorial_map(type) when is_integer(type) do
|
||||
type |> from_int() |> get_tutorial_map()
|
||||
end
|
||||
def get_tutorial_map(_), do: 100000000 # Default to Henesys
|
||||
|
||||
@doc """
|
||||
Gets the beginner guide book item ID for a job type.
|
||||
"""
|
||||
@spec get_guide_book(t() | integer()) :: integer() | nil
|
||||
def get_guide_book(:resistance), do: 4161001
|
||||
def get_guide_book(:adventurer), do: 4161001
|
||||
def get_guide_book(:cygnus), do: 4161047
|
||||
def get_guide_book(:aran), do: 4161048
|
||||
def get_guide_book(:evan), do: 4161052
|
||||
def get_guide_book(type) when is_integer(type) do
|
||||
type |> from_int() |> get_guide_book()
|
||||
end
|
||||
def get_guide_book(_), do: nil
|
||||
|
||||
@doc """
|
||||
Gets the initial quests for a job type.
|
||||
Returns a list of {quest_id, status, custom_data} tuples.
|
||||
"""
|
||||
@spec get_initial_quests(t() | integer()) :: list()
|
||||
def get_initial_quests(:cygnus) do
|
||||
[
|
||||
{20022, 1, "1"},
|
||||
{20010, 1, nil}
|
||||
]
|
||||
end
|
||||
def get_initial_quests(:ultimate_adventurer) do
|
||||
# Complete all explorer quests (2490-2507)
|
||||
base_quests = Enum.map(2490..2507, fn qid -> {qid, 2, nil} end)
|
||||
[
|
||||
{29947, 2, nil}
|
||||
| base_quests
|
||||
]
|
||||
end
|
||||
def get_initial_quests(type) when is_integer(type) do
|
||||
type |> from_int() |> get_initial_quests()
|
||||
end
|
||||
def get_initial_quests(_), do: []
|
||||
end
|
||||
@@ -16,7 +16,7 @@ defmodule Odinsea.Game.Map do
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Game.Character
|
||||
alias Odinsea.Game.{MapFactory, LifeFactory, Monster, Reactor, ReactorFactory}
|
||||
alias Odinsea.Game.{Drop, MapFactory, LifeFactory, Monster, Reactor, ReactorFactory}
|
||||
alias Odinsea.Channel.Packets, as: ChannelPackets
|
||||
|
||||
# ============================================================================
|
||||
@@ -1073,11 +1073,19 @@ defmodule Odinsea.Game.Map do
|
||||
|
||||
@doc """
|
||||
Attempts to pick up a drop.
|
||||
Returns {:ok, drop} if successful, {:error, reason} if not.
|
||||
"""
|
||||
def pickup_drop(map_id, channel_id, drop_oid, character_id) do
|
||||
GenServer.call(via_tuple(map_id, channel_id), {:pickup_drop, drop_oid, character_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a drop is visible to a character (for quest items, individual rewards).
|
||||
"""
|
||||
def drop_visible_to?(map_id, channel_id, drop_oid, character_id, quest_status \\ %{}) do
|
||||
GenServer.call(via_tuple(map_id, channel_id), {:drop_visible_to, drop_oid, character_id, quest_status})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_drops, _from, state) do
|
||||
{:reply, state.items, state}
|
||||
@@ -1092,24 +1100,41 @@ defmodule Odinsea.Game.Map do
|
||||
drop ->
|
||||
now = System.system_time(:millisecond)
|
||||
|
||||
case DropSystem.pickup_drop(drop, character_id, now) do
|
||||
{:ok, updated_drop} ->
|
||||
# Broadcast pickup animation
|
||||
remove_packet = ChannelPackets.remove_drop(drop_oid, 2, character_id)
|
||||
broadcast_to_players(state.players, remove_packet)
|
||||
|
||||
# Remove from map
|
||||
new_items = Map.delete(state.items, drop_oid)
|
||||
|
||||
# Return drop info for inventory addition
|
||||
{:reply, {:ok, updated_drop}, %{state | items: new_items}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
# Validate ownership using Drop.can_loot?
|
||||
if not Drop.can_loot?(drop, character_id, now) do
|
||||
{:reply, {:error, :not_owner}, state}
|
||||
else
|
||||
case DropSystem.pickup_drop(drop, character_id, now) do
|
||||
{:ok, updated_drop} ->
|
||||
# Broadcast pickup animation to all players
|
||||
remove_packet = ChannelPackets.remove_drop(drop_oid, 2, character_id)
|
||||
broadcast_to_players(state.players, remove_packet)
|
||||
|
||||
# Remove from map
|
||||
new_items = Map.delete(state.items, drop_oid)
|
||||
|
||||
# Return drop info for inventory addition
|
||||
{:reply, {:ok, updated_drop}, %{state | items: new_items}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:drop_visible_to, drop_oid, character_id, quest_status}, _from, state) do
|
||||
case Map.get(state.items, drop_oid) do
|
||||
nil ->
|
||||
{:reply, false, state}
|
||||
|
||||
drop ->
|
||||
visible = Drop.visible_to?(drop, character_id, quest_status)
|
||||
{:reply, visible, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_drop_expiration, state) do
|
||||
now = System.system_time(:millisecond)
|
||||
|
||||
Reference in New Issue
Block a user