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