update
This commit is contained in:
@@ -92,6 +92,29 @@ defmodule Odinsea.Channel.Client do
|
||||
cp_magic_attack = Opcodes.cp_magic_attack()
|
||||
cp_take_damage = Opcodes.cp_take_damage()
|
||||
|
||||
# Inventory opcodes
|
||||
cp_item_move = Opcodes.cp_item_move()
|
||||
cp_item_sort = Opcodes.cp_item_sort()
|
||||
cp_item_gather = Opcodes.cp_item_gather()
|
||||
cp_use_item = Opcodes.cp_use_item()
|
||||
cp_use_return_scroll = Opcodes.cp_use_return_scroll()
|
||||
cp_use_scroll = Opcodes.cp_use_upgrade_scroll()
|
||||
cp_use_cash_item = Opcodes.cp_use_cash_item()
|
||||
|
||||
# NPC opcodes
|
||||
cp_npc_move = Opcodes.cp_npc_move()
|
||||
cp_npc_talk = Opcodes.cp_npc_talk()
|
||||
cp_npc_talk_more = Opcodes.cp_npc_talk_more()
|
||||
cp_npc_shop = Opcodes.cp_npc_shop()
|
||||
cp_storage = Opcodes.cp_storage()
|
||||
cp_quest_action = Opcodes.cp_quest_action()
|
||||
cp_repair = Opcodes.cp_repair()
|
||||
cp_repair_all = Opcodes.cp_repair_all()
|
||||
cp_update_quest = Opcodes.cp_update_quest()
|
||||
cp_use_item_quest = Opcodes.cp_use_item_quest()
|
||||
cp_public_npc = Opcodes.cp_public_npc()
|
||||
cp_use_scripted_npc_item = Opcodes.cp_use_scripted_npc_item()
|
||||
|
||||
case opcode do
|
||||
# Chat handlers
|
||||
^cp_general_chat ->
|
||||
@@ -162,6 +185,98 @@ defmodule Odinsea.Channel.Client do
|
||||
_ -> state
|
||||
end
|
||||
|
||||
# Inventory handlers
|
||||
^cp_item_move ->
|
||||
case Handler.Inventory.handle_item_move(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_item_sort ->
|
||||
case Handler.Inventory.handle_item_sort(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_item_gather ->
|
||||
case Handler.Inventory.handle_item_gather(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_use_item ->
|
||||
case Handler.Inventory.handle_use_item(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_use_return_scroll ->
|
||||
case Handler.Inventory.handle_use_return_scroll(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_use_scroll ->
|
||||
case Handler.Inventory.handle_use_scroll(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
^cp_use_cash_item ->
|
||||
case Handler.Inventory.handle_use_cash_item(packet, state) do
|
||||
{:ok, new_state} -> new_state
|
||||
_ -> state
|
||||
end
|
||||
|
||||
# NPC handlers
|
||||
^cp_npc_move ->
|
||||
Handler.NPC.handle_npc_move(packet, self())
|
||||
state
|
||||
|
||||
^cp_npc_talk ->
|
||||
Handler.NPC.handle_npc_talk(packet, self())
|
||||
state
|
||||
|
||||
^cp_npc_talk_more ->
|
||||
Handler.NPC.handle_npc_more_talk(packet, self())
|
||||
state
|
||||
|
||||
^cp_npc_shop ->
|
||||
Handler.NPC.handle_npc_shop(packet, self())
|
||||
state
|
||||
|
||||
^cp_storage ->
|
||||
Handler.NPC.handle_storage(packet, self())
|
||||
state
|
||||
|
||||
^cp_quest_action ->
|
||||
Handler.NPC.handle_quest_action(packet, self())
|
||||
state
|
||||
|
||||
^cp_repair ->
|
||||
Handler.NPC.handle_repair(packet, self())
|
||||
state
|
||||
|
||||
^cp_repair_all ->
|
||||
Handler.NPC.handle_repair_all(self())
|
||||
state
|
||||
|
||||
^cp_update_quest ->
|
||||
Handler.NPC.handle_update_quest(packet, self())
|
||||
state
|
||||
|
||||
^cp_use_item_quest ->
|
||||
Handler.NPC.handle_use_item_quest(packet, self())
|
||||
state
|
||||
|
||||
^cp_public_npc ->
|
||||
Handler.NPC.handle_public_npc(packet, self())
|
||||
state
|
||||
|
||||
^cp_use_scripted_npc_item ->
|
||||
Handler.NPC.handle_use_scripted_npc_item(packet, self())
|
||||
state
|
||||
|
||||
_ ->
|
||||
Logger.debug("Unhandled channel opcode: 0x#{Integer.to_string(opcode, 16)}")
|
||||
state
|
||||
|
||||
388
lib/odinsea/channel/handler/inventory.ex
Normal file
388
lib/odinsea/channel/handler/inventory.ex
Normal file
@@ -0,0 +1,388 @@
|
||||
defmodule Odinsea.Channel.Handler.Inventory do
|
||||
@moduledoc """
|
||||
Handles inventory-related packets (item move, equip, drop, sort, etc.).
|
||||
Ported from src/handling/channel/handler/InventoryHandler.java
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Net.Packet.{In, Out}
|
||||
alias Odinsea.Net.Opcodes
|
||||
alias Odinsea.Game.{Character, Inventory, InventoryType}
|
||||
|
||||
# Slot limits for different inventory types
|
||||
@slot_limits %{
|
||||
equip: 24,
|
||||
use: 80,
|
||||
setup: 80,
|
||||
etc: 80,
|
||||
cash: 40
|
||||
}
|
||||
|
||||
@doc """
|
||||
Handles item move (CP_ItemMove).
|
||||
Ported from InventoryHandler.ItemMove()
|
||||
"""
|
||||
def handle_item_move(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state),
|
||||
{:ok, character} <- Character.get_state(character_pid) do
|
||||
# Skip update tick
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
|
||||
# Parse packet data
|
||||
{inv_type_byte, packet} = In.decode_byte(packet)
|
||||
{src_slot, packet} = In.decode_short(packet)
|
||||
{dst_slot, packet} = In.decode_short(packet)
|
||||
{quantity, _packet} = In.decode_short(packet)
|
||||
|
||||
inv_type = InventoryType.from_type(inv_type_byte)
|
||||
slot_max = Map.get(@slot_limits, inv_type, 100)
|
||||
|
||||
Logger.debug(
|
||||
"Item move: #{character.name}, type=#{inv_type}, src=#{src_slot}, dst=#{dst_slot}, qty=#{quantity}"
|
||||
)
|
||||
|
||||
# Handle different move scenarios
|
||||
cond do
|
||||
# Unequip (equipped slot is negative)
|
||||
src_slot < 0 and dst_slot > 0 ->
|
||||
handle_unequip(character_pid, src_slot, dst_slot, client_state)
|
||||
|
||||
# Equip (destination is negative)
|
||||
dst_slot < 0 ->
|
||||
handle_equip(character_pid, src_slot, dst_slot, client_state)
|
||||
|
||||
# Drop item (destination is 0)
|
||||
dst_slot == 0 ->
|
||||
handle_drop_item(character_pid, inv_type, src_slot, quantity, client_state)
|
||||
|
||||
# Regular move
|
||||
true ->
|
||||
handle_regular_move(character_pid, inv_type, src_slot, dst_slot, slot_max, client_state)
|
||||
end
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Item move failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles item sort (CP_ItemSort).
|
||||
Ported from InventoryHandler.ItemSort()
|
||||
"""
|
||||
def handle_item_sort(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state) do
|
||||
# Skip update tick
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{inv_type_byte, _packet} = In.decode_byte(packet)
|
||||
|
||||
inv_type = InventoryType.from_type(inv_type_byte)
|
||||
|
||||
Logger.debug("Item sort requested for inventory: #{inv_type}")
|
||||
|
||||
# Perform sort by moving items to fill gaps
|
||||
case sort_inventory(character_pid, inv_type) do
|
||||
:ok ->
|
||||
# Send sort complete packet
|
||||
sort_complete_packet =
|
||||
Out.new(Opcodes.lp_finish_sort())
|
||||
|> Out.encode_byte(inv_type_byte)
|
||||
|> Out.to_data()
|
||||
|
||||
send_packet(client_state, sort_complete_packet)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Item sort failed: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Item sort failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles item gather (CP_ItemGather).
|
||||
Ported from InventoryHandler.ItemGather()
|
||||
"""
|
||||
def handle_item_gather(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state) do
|
||||
# Skip update tick
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{inv_type_byte, _packet} = In.decode_byte(packet)
|
||||
|
||||
inv_type = InventoryType.from_type(inv_type_byte)
|
||||
|
||||
Logger.debug("Item gather requested for inventory: #{inv_type}")
|
||||
|
||||
# TODO: Implement item gather (stack similar items)
|
||||
# For now, just acknowledge
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Item gather failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles use item (CP_UseItem).
|
||||
Ported from InventoryHandler.UseItem()
|
||||
"""
|
||||
def handle_use_item(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state),
|
||||
{:ok, character} <- Character.get_state(character_pid) do
|
||||
# Skip update tick and slot
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{slot, _packet} = In.decode_short(packet)
|
||||
|
||||
Logger.debug("Use item: #{character.name}, slot=#{slot} (stub)")
|
||||
|
||||
# TODO: Implement item usage
|
||||
# - Get item from USE inventory
|
||||
# - Check if usable
|
||||
# - Apply effect
|
||||
# - Consume item
|
||||
# - Broadcast effect
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Use item failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles use return scroll (CP_UseReturnScroll).
|
||||
Ported from InventoryHandler.UseReturnScroll()
|
||||
"""
|
||||
def handle_use_return_scroll(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state),
|
||||
{:ok, character} <- Character.get_state(character_pid) do
|
||||
# Skip update tick and slot
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{slot, _packet} = In.decode_short(packet)
|
||||
|
||||
Logger.debug("Use return scroll: #{character.name}, slot=#{slot} (stub)")
|
||||
|
||||
# TODO: Implement return scroll
|
||||
# - Consume scroll
|
||||
# - Return to nearest town
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Use return scroll failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles use scroll (CP_UseUpgradeScroll).
|
||||
Ported from InventoryHandler.UseUpgradeScroll()
|
||||
"""
|
||||
def handle_use_scroll(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state),
|
||||
{:ok, character} <- Character.get_state(character_pid) do
|
||||
# Parse scroll packet
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{scroll_slot, packet} = In.decode_short(packet)
|
||||
{equip_slot, packet} = In.decode_short(packet)
|
||||
{white_scroll, packet} = In.decode_byte(packet)
|
||||
{legendary_spirit, _packet} = In.decode_byte(packet)
|
||||
|
||||
Logger.debug(
|
||||
"Use scroll: #{character.name}, scroll=#{scroll_slot}, equip=#{equip_slot}, " <>
|
||||
"white=#{white_scroll}, spirit=#{legendary_spirit} (stub)"
|
||||
)
|
||||
|
||||
# TODO: Implement scrolling
|
||||
# - Get scroll and equip
|
||||
# - Check compatibility
|
||||
# - Apply success/fail logic
|
||||
# - Update equip stats
|
||||
# - Consume scroll
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Use scroll failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles use cash item (CP_UseCashItem).
|
||||
Ported from InventoryHandler.UseCashItem()
|
||||
"""
|
||||
def handle_use_cash_item(packet, client_state) do
|
||||
with {:ok, character_pid} <- get_character(client_state),
|
||||
{:ok, character} <- Character.get_state(character_pid) do
|
||||
# Skip update tick
|
||||
{_tick, packet} = In.decode_int(packet)
|
||||
{slot, _packet} = In.decode_short(packet)
|
||||
|
||||
Logger.debug("Use cash item: #{character.name}, slot=#{slot} (stub)")
|
||||
|
||||
# TODO: Implement cash item usage
|
||||
# - Get cash item
|
||||
# - Apply effect based on item type
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.warning("Use cash item failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
defp handle_equip(character_pid, src_slot, dst_slot, client_state) do
|
||||
case Character.equip_item(character_pid, src_slot, dst_slot) do
|
||||
:ok ->
|
||||
Logger.debug("Equipped item: src=#{src_slot}, dst=#{dst_slot}")
|
||||
# TODO: Broadcast equip update to map
|
||||
# TODO: Recalculate and update character stats
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Equip failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_unequip(character_pid, src_slot, dst_slot, client_state) do
|
||||
case Character.unequip_item(character_pid, src_slot, dst_slot) do
|
||||
:ok ->
|
||||
Logger.debug("Unequipped item: src=#{src_slot}, dst=#{dst_slot}")
|
||||
# TODO: Broadcast unequip update to map
|
||||
# TODO: Recalculate and update character stats
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Unequip failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_drop_item(character_pid, inv_type, src_slot, quantity, client_state) do
|
||||
case Character.drop_item(character_pid, inv_type, src_slot, quantity) do
|
||||
{:ok, dropped_item} ->
|
||||
Logger.debug("Dropped item: #{dropped_item.item_id}, qty=#{quantity}")
|
||||
|
||||
# TODO: Create map item (drop on ground)
|
||||
# TODO: Broadcast drop to map
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Drop item failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_regular_move(character_pid, inv_type, src_slot, dst_slot, slot_max, client_state) do
|
||||
case Character.move_item(character_pid, inv_type, src_slot, dst_slot, slot_max) do
|
||||
:ok ->
|
||||
Logger.debug("Moved item: #{inv_type}, #{src_slot} -> #{dst_slot}")
|
||||
|
||||
# Send inventory update to client
|
||||
# TODO: Send proper inventory update packet
|
||||
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Move item failed: #{inspect(reason)}")
|
||||
send_enable_actions(client_state)
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp sort_inventory(character_pid, inv_type) do
|
||||
# Get the inventory
|
||||
with {:ok, inventory} <- Character.get_inventory(character_pid, inv_type),
|
||||
slots = Inventory.list(inventory),
|
||||
sorted_slots = Enum.sort_by(slots, fn item -> item.position end) do
|
||||
# Move items to fill gaps
|
||||
sort_items(character_pid, inv_type, sorted_slots, 1)
|
||||
else
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp sort_items(_character_pid, _inv_type, [], _target_slot), do: :ok
|
||||
|
||||
defp sort_items(character_pid, inv_type, [item | rest], target_slot) do
|
||||
if item.position != target_slot and item.position > 0 do
|
||||
# Move item to target slot
|
||||
case Character.move_item(character_pid, inv_type, item.position, target_slot, 100) do
|
||||
:ok -> sort_items(character_pid, inv_type, rest, target_slot + 1)
|
||||
{:error, _} -> sort_items(character_pid, inv_type, rest, target_slot + 1)
|
||||
end
|
||||
else
|
||||
sort_items(character_pid, inv_type, rest, target_slot + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_character(client_state) do
|
||||
case client_state.character_id do
|
||||
nil ->
|
||||
{:error, :no_character}
|
||||
|
||||
character_id ->
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, character_id) do
|
||||
[{pid, _}] -> {:ok, pid}
|
||||
[] -> {:error, :character_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp send_packet(client_state, data) when is_pid(client_state) do
|
||||
# If client_state is a PID, send directly
|
||||
send(client_state, {:send_packet, data})
|
||||
end
|
||||
|
||||
defp send_packet(client_state, data) do
|
||||
# Otherwise, send to the client_pid in the state
|
||||
if client_state.client_pid do
|
||||
send(client_state.client_pid, {:send_packet, data})
|
||||
end
|
||||
end
|
||||
|
||||
defp send_enable_actions(client_state) do
|
||||
# Send enable actions packet to allow further client actions
|
||||
# This is a minimal packet to unblock the client
|
||||
enable_packet = <<0x0D, 0x00, 0x00>>
|
||||
send_packet(client_state, enable_packet)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user