port over some more

This commit is contained in:
ra
2026-02-14 23:58:01 -07:00
parent 0222be36c5
commit 61176cd416
107 changed files with 9124 additions and 375 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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)