kimi gone wild
This commit is contained in:
528
lib/odinsea/channel/handler/pet.ex
Normal file
528
lib/odinsea/channel/handler/pet.ex
Normal file
@@ -0,0 +1,528 @@
|
||||
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
|
||||
Reference in New Issue
Block a user