Start repo, claude & kimi still vibing tho
This commit is contained in:
447
lib/odinsea/channel/handler/npc.ex
Normal file
447
lib/odinsea/channel/handler/npc.ex
Normal file
@@ -0,0 +1,447 @@
|
||||
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
|
||||
Reference in New Issue
Block a user