Files
odinsea-elixir/lib/odinsea/channel/handler/npc.ex

448 lines
12 KiB
Elixir

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