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