kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View 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