defmodule Odinsea.Channel.Handler.NPC do @moduledoc """ Handles NPC interaction packets: talk, shop, storage, quests. Ported from src/handling/channel/handler/NPCHandler.java """ require Logger alias Odinsea.Net.Packet.{In, Out} alias Odinsea.Net.Opcodes alias Odinsea.Constants.Game @doc """ Handles NPC movement/talk animations. Forwards NPC movement/animation packets to other players on the map. """ def handle_npc_move(%In{} = packet, client_pid) do with {:ok, chr_pid} <- get_character(client_pid), {:ok, map_pid} <- get_map(chr_pid), {:ok, change_time} <- get_change_time(chr_pid) do now = System.system_time(:millisecond) # Anti-spam: prevent rapid NPC interactions if change_time > 0 and now - change_time < 7000 do :ok else handle_npc_move_packet(packet, client_pid, map_pid) end else _error -> :ok end end defp handle_npc_move_packet(packet, _client_pid, _map_pid) do packet_length = In.remaining(packet) cond do # NPC Talk (10 bytes for GMS, 6 for KMS) packet_length == 10 -> oid = In.decode_int(packet) byte1 = In.decode_byte(packet) unk = In.decode_byte(packet) if unk == -1 do :ok else unk2 = In.decode_int(packet) # TODO: Validate NPC exists on map # TODO: Broadcast NPC action to other players Logger.debug("NPC talk: oid=#{oid}, byte1=#{byte1}, unk=#{unk}, unk2=#{unk2}") :ok end # NPC Move (more than 10 bytes) packet_length > 10 -> movement_data = In.decode_buffer(packet, packet_length - 9) # TODO: Broadcast NPC movement to other players Logger.debug("NPC move: #{byte_size(movement_data)} bytes of movement data") :ok true -> :ok end end @doc """ Handles NPC shop actions: buy, sell, recharge. """ def handle_npc_shop(%In{} = packet, client_pid) do mode = In.decode_byte(packet) case mode do # Buy item from shop 0 -> In.skip(packet, 2) item_id = In.decode_int(packet) quantity = In.decode_short(packet) handle_shop_buy(client_pid, item_id, quantity) # Sell item to shop 1 -> slot = In.decode_short(packet) item_id = In.decode_int(packet) quantity = In.decode_short(packet) handle_shop_sell(client_pid, slot, item_id, quantity) # Recharge item (stars/bullets) 2 -> slot = In.decode_short(packet) handle_shop_recharge(client_pid, slot) # Close shop _ -> handle_shop_close(client_pid) end end defp handle_shop_buy(_client_pid, item_id, quantity) do # TODO: Implement shop buy # 1. Get character's current shop # 2. Validate item exists in shop # 3. Check mesos # 4. Check inventory space # 5. Deduct mesos and add item Logger.debug("Shop buy: item=#{item_id}, qty=#{quantity} (STUB)") :ok end defp handle_shop_sell(_client_pid, slot, item_id, quantity) do # TODO: Implement shop sell # 1. Get character's current shop # 2. Validate item in inventory # 3. Calculate sell price # 4. Remove item and add mesos Logger.debug("Shop sell: slot=#{slot}, item=#{item_id}, qty=#{quantity} (STUB)") :ok end defp handle_shop_recharge(_client_pid, slot) do # TODO: Implement recharge # 1. Get character's current shop # 2. Validate item is rechargeable (stars/bullets) # 3. Calculate recharge cost # 4. Recharge to full quantity Logger.debug("Shop recharge: slot=#{slot} (STUB)") :ok end defp handle_shop_close(client_pid) do # TODO: Clear character's shop reference Logger.debug("Shop close for client #{inspect(client_pid)} (STUB)") :ok end @doc """ Handles NPC talk initiation. Opens NPC shop or starts NPC script dialog. """ def handle_npc_talk(%In{} = packet, client_pid) do with {:ok, chr_pid} <- get_character(client_pid), {:ok, map_pid} <- get_map(chr_pid), {:ok, last_select_time} <- get_last_select_npc_time(chr_pid) do now = System.system_time(:millisecond) # Anti-spam: minimum 500ms between NPC interactions if last_select_time == 0 or now - last_select_time >= 500 do oid = In.decode_int(packet) _tick = In.decode_int(packet) # TODO: Update last select NPC time # TODO: Get NPC from map by OID # TODO: Check if NPC has shop # TODO: If shop, open shop; else start script Logger.debug("NPC talk: oid=#{oid} (STUB - needs script/shop system)") :ok else :ok end else _error -> :ok end end @doc """ Handles quest actions: start, complete, forfeit, restore item. """ def handle_quest_action(%In{} = packet, client_pid) do action = In.decode_byte(packet) quest_id = In.decode_ushort(packet) case action do # Restore lost item 0 -> _tick = In.decode_int(packet) item_id = In.decode_int(packet) handle_quest_restore_item(client_pid, quest_id, item_id) # Start quest 1 -> npc_id = In.decode_int(packet) handle_quest_start(client_pid, quest_id, npc_id) # Complete quest 2 -> npc_id = In.decode_int(packet) _tick = In.decode_int(packet) selection = if In.remaining(packet) >= 4 do In.decode_int(packet) else nil end handle_quest_complete(client_pid, quest_id, npc_id, selection) # Forfeit quest 3 -> handle_quest_forfeit(client_pid, quest_id) # Scripted start quest 4 -> npc_id = In.decode_int(packet) handle_quest_start_scripted(client_pid, quest_id, npc_id) # Scripted end quest 5 -> npc_id = In.decode_int(packet) handle_quest_end_scripted(client_pid, quest_id, npc_id) _ -> Logger.warn("Unknown quest action: #{action}") :ok end end defp handle_quest_restore_item(_client_pid, quest_id, item_id) do Logger.debug("Quest restore item: quest=#{quest_id}, item=#{item_id} (STUB)") :ok end defp handle_quest_start(_client_pid, quest_id, npc_id) do # TODO: Load quest, check requirements, start quest Logger.debug("Quest start: quest=#{quest_id}, npc=#{npc_id} (STUB)") :ok end defp handle_quest_complete(_client_pid, quest_id, npc_id, selection) do # TODO: Load quest, check completion, give rewards Logger.debug( "Quest complete: quest=#{quest_id}, npc=#{npc_id}, selection=#{inspect(selection)} (STUB)" ) :ok end defp handle_quest_forfeit(_client_pid, quest_id) do # TODO: Check if quest can be forfeited, remove from character Logger.debug("Quest forfeit: quest=#{quest_id} (STUB)") :ok end defp handle_quest_start_scripted(_client_pid, quest_id, npc_id) do # TODO: Start quest script via script manager Logger.debug("Quest start scripted: quest=#{quest_id}, npc=#{npc_id} (STUB)") :ok end defp handle_quest_end_scripted(_client_pid, quest_id, npc_id) do # TODO: End quest script via script manager # TODO: Broadcast quest completion effect Logger.debug("Quest end scripted: quest=#{quest_id}, npc=#{npc_id} (STUB)") :ok end @doc """ Handles storage actions: take out, store, arrange, mesos. """ def handle_storage(%In{} = packet, client_pid) do mode = In.decode_byte(packet) case mode do # Take out item 4 -> type = In.decode_byte(packet) slot = In.decode_byte(packet) handle_storage_take_out(client_pid, type, slot) # Store item 5 -> slot = In.decode_short(packet) item_id = In.decode_int(packet) quantity = In.decode_short(packet) handle_storage_store(client_pid, slot, item_id, quantity) # Arrange storage 6 -> handle_storage_arrange(client_pid) # Meso deposit/withdraw 7 -> meso = In.decode_int(packet) handle_storage_meso(client_pid, meso) # Close storage 8 -> handle_storage_close(client_pid) _ -> Logger.warn("Unknown storage mode: #{mode}") :ok end end defp handle_storage_take_out(_client_pid, type, slot) do # TODO: Get storage, validate slot, check inventory space, move item Logger.debug("Storage take out: type=#{type}, slot=#{slot} (STUB)") :ok end defp handle_storage_store(_client_pid, slot, item_id, quantity) do # TODO: Validate item, check storage space, charge fee, move item Logger.debug("Storage store: slot=#{slot}, item=#{item_id}, qty=#{quantity} (STUB)") :ok end defp handle_storage_arrange(_client_pid) do # TODO: Sort storage items Logger.debug("Storage arrange (STUB)") :ok end defp handle_storage_meso(_client_pid, meso) do # TODO: Transfer mesos between character and storage Logger.debug("Storage meso: #{meso} (STUB)") :ok end defp handle_storage_close(_client_pid) do # TODO: Close storage, clear reference Logger.debug("Storage close (STUB)") :ok end @doc """ Handles NPC dialog continuation (script responses). """ def handle_npc_more_talk(%In{} = packet, client_pid) do last_msg = In.decode_byte(packet) action = In.decode_byte(packet) cond do # Text input response last_msg == 3 -> if action != 0 do text = In.decode_string(packet) # TODO: Pass text to script manager Logger.debug("NPC more talk (text): #{text} (STUB)") end # Selection response true -> selection = cond do In.remaining(packet) >= 4 -> In.decode_int(packet) In.remaining(packet) > 0 -> In.decode_byte(packet) true -> -1 end # TODO: Pass selection to script manager Logger.debug("NPC more talk (selection): #{selection}, action=#{action} (STUB)") end :ok end @doc """ Handles equipment repair (single item). """ def handle_repair(%In{} = packet, client_pid) do if In.remaining(packet) < 4 do :ok else position = In.decode_int(packet) # TODO: Validate map, check durability, calculate cost, repair item Logger.debug("Repair: position=#{position} (STUB)") :ok end end @doc """ Handles equipment repair (all items). """ def handle_repair_all(client_pid) do # TODO: Find all damaged items, calculate total cost, repair all Logger.debug("Repair all (STUB)") :ok end @doc """ Handles quest info update. """ def handle_update_quest(%In{} = packet, client_pid) do quest_id = In.decode_short(packet) # TODO: Update quest progress/info Logger.debug("Update quest: #{quest_id} (STUB)") :ok end @doc """ Handles using quest items. """ def handle_use_item_quest(%In{} = packet, client_pid) do slot = In.decode_short(packet) item_id = In.decode_int(packet) quest_id = In.decode_int(packet) new_data = In.decode_int(packet) # TODO: Validate quest item, update quest data, consume item Logger.debug( "Use item quest: slot=#{slot}, item=#{item_id}, quest=#{quest_id}, data=#{new_data} (STUB)" ) :ok end @doc """ Handles opening public NPCs (from UI, not on map). """ def handle_public_npc(%In{} = packet, client_pid) do npc_id = In.decode_int(packet) # TODO: Validate NPC in public NPC list, start script Logger.debug("Public NPC: #{npc_id} (STUB)") :ok end @doc """ Handles using scripted NPC items. """ def handle_use_scripted_npc_item(%In{} = packet, client_pid) do slot = In.decode_short(packet) item_id = In.decode_int(packet) # TODO: Validate item, run NPC script for item Logger.debug("Use scripted NPC item: slot=#{slot}, item=#{item_id} (STUB)") :ok end # Helper functions to get character/map info defp get_character(client_pid) do # TODO: Get character PID from client state {:ok, nil} end defp get_map(_chr_pid) do # TODO: Get map PID from character state {:ok, nil} end defp get_change_time(_chr_pid) do # TODO: Get last map change time from character {:ok, 0} end defp get_last_select_npc_time(_chr_pid) do # TODO: Get last NPC select time from character {:ok, 0} end end