448 lines
12 KiB
Elixir
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
|