529 lines
17 KiB
Elixir
529 lines
17 KiB
Elixir
defmodule Odinsea.Channel.Handler.Pet do
|
|
@moduledoc """
|
|
Handles pet-related packets.
|
|
Ported from src/handling/channel/handler/PetHandler.java
|
|
|
|
Handles:
|
|
- Pet spawning/despawning
|
|
- Pet movement
|
|
- Pet commands (tricks)
|
|
- Pet chat
|
|
- Pet food (feeding)
|
|
- Pet auto-potion
|
|
- Pet item looting
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Net.Packet.{In, Out}
|
|
alias Odinsea.Net.Opcodes
|
|
alias Odinsea.Game.{Character, Pet, PetData}
|
|
alias Odinsea.Channel.Packets
|
|
|
|
# ============================================================================
|
|
# Pet Spawning
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet spawn request (CP_SpawnPet).
|
|
Ported from PetHandler.SpawnPet()
|
|
"""
|
|
def handle_spawn_pet(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode packet
|
|
{_tick, packet} = In.decode_int(packet)
|
|
{slot, packet} = In.decode_byte(packet)
|
|
{lead, _packet} = In.decode_byte(packet)
|
|
|
|
Logger.info("Pet spawn request: character=#{character.name}, slot=#{slot}, lead=#{lead}")
|
|
|
|
# Get pet from inventory and spawn it
|
|
case Character.spawn_pet(character_pid, slot, lead > 0) do
|
|
{:ok, pet} ->
|
|
Logger.info("Pet spawned: #{pet.name} (level #{pet.level})")
|
|
|
|
# Broadcast pet spawn to map
|
|
spawn_packet = Packets.spawn_pet(character.id, pet, false, false)
|
|
broadcast_to_map(character.map_id, character.id, spawn_packet, client_state)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to spawn pet: #{inspect(reason)}")
|
|
end
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Spawn pet failed: #{inspect(reason)}")
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Handles pet despawn.
|
|
"""
|
|
def handle_despawn_pet(pet_index, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
case Character.despawn_pet(character_pid, pet_index) do
|
|
{:ok, pet} ->
|
|
Logger.info("Pet despawned: #{pet.name}")
|
|
|
|
# Broadcast pet removal to map
|
|
remove_packet = Packets.remove_pet(character.id, pet_index)
|
|
broadcast_to_map(character.map_id, character.id, remove_packet, client_state)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to despawn pet: #{inspect(reason)}")
|
|
end
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Despawn pet failed: #{inspect(reason)}")
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Movement
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet movement (CP_MovePet).
|
|
Ported from PetHandler.MovePet()
|
|
"""
|
|
def handle_move_pet(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode pet ID/slot
|
|
{pet_id_or_slot, packet} = decode_pet_id(packet, Odinsea.Constants.Game.gms?())
|
|
|
|
# Skip field key check bytes
|
|
{_, packet} = In.skip(packet, 8)
|
|
|
|
# Get movement data (binary blob to forward to other clients)
|
|
# In full implementation, parse and validate movement
|
|
movement_data = packet
|
|
|
|
pet_slot = if Odinsea.Constants.Game.gms?(), do: pet_id_or_slot, else: pet_id_or_slot
|
|
|
|
# Get the pet
|
|
case Character.get_pet(character_pid, pet_slot) do
|
|
{:ok, pet} ->
|
|
# Update pet position (in full implementation)
|
|
# Character.update_pet_position(character_pid, pet_slot, new_position)
|
|
|
|
# Broadcast movement to other players
|
|
move_packet = Packets.move_pet(character.id, pet.unique_id, pet_slot, movement_data)
|
|
broadcast_to_map(character.map_id, character.id, move_packet, client_state)
|
|
|
|
# Check for item pickup if pet has pickup ability
|
|
if Pet.has_flag?(pet, PetData.PetFlag.item_pickup()) do
|
|
check_pet_loot(character, pet, pet_slot, client_state)
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
:ok
|
|
end
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Move pet failed: #{inspect(reason)}")
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Chat
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet chat (CP_PetChat).
|
|
Ported from PetHandler.PetChat()
|
|
"""
|
|
def handle_pet_chat(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode packet
|
|
{pet_slot, packet} = decode_pet_slot(packet)
|
|
{chat_command, packet} = In.decode_short(packet)
|
|
{text, _packet} = In.decode_string(packet)
|
|
|
|
# Validate pet exists
|
|
case Character.get_pet(character_pid, pet_slot) do
|
|
{:ok, _pet} ->
|
|
Logger.debug("Pet chat: #{character.name}'s pet says: #{text}")
|
|
|
|
# Broadcast chat to map
|
|
chat_packet = Packets.pet_chat(character.id, pet_slot, chat_command, text)
|
|
broadcast_to_map(character.map_id, character.id, chat_packet, client_state)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Pet chat failed - no pet at slot #{pet_slot}: #{inspect(reason)}")
|
|
end
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Pet chat failed: #{inspect(reason)}")
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Commands (Tricks)
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet command/trick (CP_PetCommand).
|
|
Ported from PetHandler.PetCommand()
|
|
"""
|
|
def handle_pet_command(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode packet
|
|
{pet_slot, packet} = decode_pet_slot(packet)
|
|
{command_id, _packet} = In.decode_byte(packet)
|
|
|
|
Logger.debug("Pet command: character=#{character.name}, slot=#{pet_slot}, cmd=#{command_id}")
|
|
|
|
case Character.get_pet(character_pid, pet_slot) do
|
|
{:ok, pet} ->
|
|
# Get command data
|
|
case PetData.get_pet_command(pet.pet_item_id, command_id) do
|
|
{probability, closeness_inc} ->
|
|
# Roll for success
|
|
success = :rand.uniform(100) <= probability
|
|
|
|
{_result, updated_pet} =
|
|
if success do
|
|
# Add closeness on success
|
|
case Pet.add_closeness(pet, closeness_inc) do
|
|
{:level_up, leveled_pet} ->
|
|
# Send level up packets
|
|
own_level_packet = Packets.show_own_pet_level_up(pet_slot)
|
|
send_packet(client_state, own_level_packet)
|
|
|
|
other_level_packet = Packets.show_pet_level_up(character.id, pet_slot)
|
|
broadcast_to_map(character.map_id, character.id, other_level_packet, client_state)
|
|
|
|
{:level_up, leveled_pet}
|
|
|
|
{:ok, updated} ->
|
|
{:ok, updated}
|
|
end
|
|
else
|
|
{:fail, pet}
|
|
end
|
|
|
|
# Save pet if changed
|
|
if updated_pet.changed do
|
|
Character.update_pet(character_pid, updated_pet)
|
|
|
|
# Send pet update packet
|
|
update_packet = Packets.update_pet(updated_pet)
|
|
send_packet(client_state, update_packet)
|
|
end
|
|
|
|
# Send command response
|
|
response_packet =
|
|
Packets.pet_command_response(
|
|
character.id,
|
|
pet_slot,
|
|
command_id,
|
|
success,
|
|
false
|
|
)
|
|
|
|
broadcast_to_map(character.map_id, character.id, response_packet, client_state)
|
|
|
|
nil ->
|
|
# Unknown command
|
|
Logger.warning("Unknown pet command #{command_id} for pet #{pet.pet_item_id}")
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Pet command failed - no pet at slot #{pet_slot}: #{inspect(reason)}")
|
|
end
|
|
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Pet command failed: #{inspect(reason)}")
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Food
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet food (CP_PetFood).
|
|
Ported from PetHandler.PetFood()
|
|
"""
|
|
def handle_pet_food(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode packet
|
|
{_tick, packet} = In.decode_int(packet)
|
|
{slot, packet} = In.decode_short(packet)
|
|
{item_id, _packet} = In.decode_int(packet)
|
|
|
|
Logger.debug("Pet food: item=#{item_id}, slot=#{slot}")
|
|
|
|
# Find the hungriest summoned pet
|
|
case find_hungriest_pet(character_pid) do
|
|
{:ok, {pet_slot, pet}} ->
|
|
# Validate food item
|
|
if PetData.pet_food?(item_id) do
|
|
# Calculate fullness gain
|
|
food_value = PetData.get_food_value(item_id)
|
|
|
|
# 50% chance to gain closeness when feeding
|
|
gain_closeness = :rand.uniform(100) <= 50
|
|
|
|
if pet.fullness < 100 do
|
|
# Pet was hungry, feed it
|
|
updated_pet = Pet.add_fullness(pet, food_value)
|
|
|
|
# Possibly add closeness
|
|
{_closeness_result, final_pet} =
|
|
if gain_closeness do
|
|
case Pet.add_closeness(updated_pet, 1) do
|
|
{:level_up, leveled_pet} ->
|
|
own_level_packet = Packets.show_own_pet_level_up(pet_slot)
|
|
send_packet(client_state, own_level_packet)
|
|
|
|
other_level_packet = Packets.show_pet_level_up(character.id, pet_slot)
|
|
broadcast_to_map(character.map_id, character.id, other_level_packet, client_state)
|
|
|
|
{:level_up, leveled_pet}
|
|
|
|
{:ok, updated} ->
|
|
{:ok, updated}
|
|
end
|
|
else
|
|
{:ok, updated_pet}
|
|
end
|
|
|
|
# Save pet
|
|
Character.update_pet(character_pid, final_pet)
|
|
|
|
# Send update packet
|
|
update_packet = Packets.update_pet(final_pet)
|
|
send_packet(client_state, update_packet)
|
|
|
|
# Send command response (food success)
|
|
response_packet =
|
|
Packets.pet_command_response(
|
|
character.id,
|
|
pet_slot,
|
|
1,
|
|
true,
|
|
true
|
|
)
|
|
|
|
broadcast_to_map(character.map_id, character.id, response_packet, client_state)
|
|
else
|
|
# Pet was full, may lose closeness
|
|
final_pet =
|
|
if gain_closeness do
|
|
case Pet.remove_closeness(pet, 1) do
|
|
{:level_down, downgraded_pet} ->
|
|
Character.update_pet(character_pid, downgraded_pet)
|
|
downgraded_pet
|
|
|
|
{:ok, updated} ->
|
|
Character.update_pet(character_pid, updated)
|
|
updated
|
|
end
|
|
else
|
|
pet
|
|
end
|
|
|
|
# Send update
|
|
update_packet = Packets.update_pet(final_pet)
|
|
send_packet(client_state, update_packet)
|
|
|
|
# Send failure response
|
|
response_packet =
|
|
Packets.pet_command_response(
|
|
character.id,
|
|
pet_slot,
|
|
1,
|
|
false,
|
|
true
|
|
)
|
|
|
|
broadcast_to_map(character.map_id, character.id, response_packet, client_state)
|
|
end
|
|
|
|
# Remove food from inventory
|
|
# Character.remove_item(character_pid, :use, slot, 1)
|
|
else
|
|
Logger.warning("Invalid pet food item: #{item_id}")
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Pet food failed: #{inspect(reason)}")
|
|
end
|
|
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Pet food failed: #{inspect(reason)}")
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Auto-Potion
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet auto-potion (CP_PetAutoPot).
|
|
Ported from PetHandler.Pet_AutoPotion()
|
|
"""
|
|
def handle_pet_auto_potion(packet, client_state) do
|
|
# Skip field key bytes
|
|
{_, packet} = In.skip(packet, if(Odinsea.Constants.Game.gms?(), do: 9, else: 1))
|
|
|
|
# Decode packet
|
|
{_tick, packet} = In.decode_int(packet)
|
|
{slot, packet} = In.decode_short(packet)
|
|
{item_id, _packet} = In.decode_int(packet)
|
|
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, _character} <- Character.get_state(character_pid) do
|
|
# TODO: Validate item and use potion
|
|
# This requires checking if HP/MP is below threshold and using the item
|
|
Logger.debug("Pet auto-potion: slot=#{slot}, item=#{item_id}")
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Pet auto-potion failed: #{inspect(reason)}")
|
|
send_enable_actions(client_state)
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pet Loot
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Handles pet looting (CP_PetLoot).
|
|
"""
|
|
def handle_pet_loot(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
# Decode packet
|
|
{pet_slot, _packet} = decode_pet_slot(packet)
|
|
|
|
case Character.get_pet(character_pid, pet_slot) do
|
|
{:ok, pet} ->
|
|
if Pet.has_flag?(pet, PetData.PetFlag.item_pickup()) do
|
|
# Attempt to loot nearby items
|
|
check_pet_loot(character, pet, pet_slot, client_state)
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
:ok
|
|
end
|
|
|
|
{:ok, client_state}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Pet loot failed: #{inspect(reason)}")
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
defp find_hungriest_pet(character_pid) do
|
|
# Get all summoned pets and find the one with lowest fullness
|
|
case Character.get_summoned_pets(character_pid) do
|
|
[] ->
|
|
{:error, :no_pets_summoned}
|
|
|
|
pets ->
|
|
{slot, pet} = Enum.min_by(pets, fn {_slot, p} -> p.fullness end)
|
|
{:ok, {slot, pet}}
|
|
end
|
|
end
|
|
|
|
defp check_pet_loot(_character, pet, pet_slot, _client_state) do
|
|
# Get items near the pet
|
|
# In full implementation, query map for items in range
|
|
# For now, this is a placeholder
|
|
Logger.debug("Checking pet loot for #{pet.name} at slot #{pet_slot}")
|
|
|
|
# Pickup range check
|
|
# If item is in range and pet has appropriate flags, pick it up
|
|
:ok
|
|
end
|
|
|
|
# Decodes pet ID/slot based on GMS mode
|
|
defp decode_pet_id(packet, true = _gms) do
|
|
In.decode_byte(packet)
|
|
end
|
|
|
|
defp decode_pet_id(packet, false = _gms) do
|
|
In.decode_int(packet)
|
|
end
|
|
|
|
# Decodes pet slot based on GMS mode
|
|
defp decode_pet_slot(packet) do
|
|
if Odinsea.Constants.Game.gms?() do
|
|
In.decode_byte(packet)
|
|
else
|
|
In.decode_int(packet)
|
|
end
|
|
end
|
|
|
|
# Gets character PID from client state
|
|
defp get_character(client_state) do
|
|
if client_state[:character_pid] do
|
|
{:ok, client_state.character_pid}
|
|
else
|
|
{:error, :no_character}
|
|
end
|
|
end
|
|
|
|
# Sends a packet to the client
|
|
defp send_packet(client_state, packet_data) do
|
|
if client_state[:transport] && client_state[:client_pid] do
|
|
send(client_state.client_pid, {:send_packet, packet_data})
|
|
end
|
|
end
|
|
|
|
# Broadcasts a packet to all players on the map except the sender
|
|
defp broadcast_to_map(map_id, character_id, packet_data, client_state) do
|
|
# In full implementation, get map PID and broadcast
|
|
# For now, placeholder
|
|
if client_state[:channel_id] do
|
|
Odinsea.Game.Map.broadcast_except(map_id, client_state.channel_id, character_id, packet_data)
|
|
end
|
|
rescue
|
|
_ -> :ok
|
|
end
|
|
|
|
# Sends enable actions packet to client
|
|
defp send_enable_actions(client_state) do
|
|
packet = Packets.enable_actions()
|
|
send_packet(client_state, packet)
|
|
end
|
|
end
|