Files
odinsea-elixir/lib/odinsea/game/pet.ex
2026-02-14 23:12:33 -07:00

333 lines
8.1 KiB
Elixir

defmodule Odinsea.Game.Pet do
@moduledoc """
Represents a pet in the game.
Ported from src/client/inventory/MaplePet.java
Pets are companions that follow players, can pick up items, and provide buffs.
Each pet has:
- Level and closeness (affection) that grows through interaction
- Fullness (hunger) that must be maintained by feeding
- Flags for special abilities (item pickup, auto-buff, etc.)
"""
alias Odinsea.Game.PetData
@type t :: %__MODULE__{
# Identity
unique_id: integer(),
pet_item_id: integer(),
name: String.t(),
# Stats
level: byte(),
closeness: integer(),
fullness: byte(),
# Position (when summoned)
position: %{x: integer(), y: integer(), fh: integer()},
stance: integer(),
# State
summoned: byte(),
inventory_position: integer(),
seconds_left: integer(),
# Abilities (bitmask flags)
flags: integer(),
# Change tracking
changed: boolean()
}
defstruct [
:unique_id,
:pet_item_id,
:name,
:level,
:closeness,
:fullness,
:position,
:stance,
:summoned,
:inventory_position,
:seconds_left,
:flags,
:changed
]
@max_closeness 30_000
@max_fullness 100
@default_fullness 100
@default_level 1
@doc """
Creates a new pet with default values.
"""
def new(pet_item_id, unique_id, name \\ nil) do
name = name || PetData.get_default_pet_name(pet_item_id)
%__MODULE__{
unique_id: unique_id,
pet_item_id: pet_item_id,
name: name,
level: @default_level,
closeness: 0,
fullness: @default_fullness,
position: %{x: 0, y: 0, fh: 0},
stance: 0,
summoned: 0,
inventory_position: 0,
seconds_left: 0,
flags: 0,
changed: true
}
end
@doc """
Creates a pet from database values.
"""
def from_db(pet_item_id, unique_id, attrs) do
%__MODULE__{
unique_id: unique_id,
pet_item_id: pet_item_id,
name: attrs[:name] || "",
level: attrs[:level] || @default_level,
closeness: attrs[:closeness] || 0,
fullness: attrs[:fullness] || @default_fullness,
position: %{x: 0, y: 0, fh: 0},
stance: 0,
summoned: 0,
inventory_position: attrs[:inventory_position] || 0,
seconds_left: attrs[:seconds_left] || 0,
flags: attrs[:flags] || 0,
changed: false
}
end
@doc """
Sets the pet's name.
"""
def set_name(%__MODULE__{} = pet, name) do
%{pet | name: name, changed: true}
end
@doc """
Sets the pet's summoned state.
- 0 = not summoned
- 1, 2, 3 = summoned in corresponding slot
"""
def set_summoned(%__MODULE__{} = pet, summoned) when summoned in [0, 1, 2, 3] do
%{pet | summoned: summoned}
end
@doc """
Checks if the pet is currently summoned.
"""
def summoned?(%__MODULE__{} = pet) do
pet.summoned > 0
end
@doc """
Sets the inventory position of the pet item.
"""
def set_inventory_position(%__MODULE__{} = pet, position) do
%{pet | inventory_position: position}
end
@doc """
Adds closeness (affection) to the pet.
Returns {:level_up, pet} if pet leveled up, {:ok, pet} otherwise.
"""
def add_closeness(%__MODULE__{} = pet, amount) do
new_closeness = min(@max_closeness, pet.closeness + amount)
next_level_req = PetData.closeness_for_level(pet.level + 1)
pet = %{pet | closeness: new_closeness, changed: true}
if new_closeness >= next_level_req and pet.level < 30 do
{:level_up, level_up(pet)}
else
{:ok, pet}
end
end
@doc """
Removes closeness from the pet (e.g., when fullness is 0).
May cause level down.
Returns {:level_down, pet} if pet leveled down, {:ok, pet} otherwise.
"""
def remove_closeness(%__MODULE__{} = pet, amount) do
new_closeness = max(0, pet.closeness - amount)
current_level_req = PetData.closeness_for_level(pet.level)
pet = %{pet | closeness: new_closeness, changed: true}
if new_closeness < current_level_req and pet.level > 1 do
{:level_down, %{pet | level: pet.level - 1}}
else
{:ok, pet}
end
end
@doc """
Levels up the pet.
"""
def level_up(%__MODULE__{} = pet) do
%{pet | level: min(30, pet.level + 1), changed: true}
end
@doc """
Adds fullness to the pet (when fed).
Max fullness is 100.
"""
def add_fullness(%__MODULE__{} = pet, amount) do
new_fullness = min(@max_fullness, pet.fullness + amount)
%{pet | fullness: new_fullness, changed: true}
end
@doc """
Decreases fullness (called periodically by hunger timer).
May decrease closeness if fullness reaches 0.
"""
def decrease_fullness(%__MODULE__{} = pet, amount) do
new_fullness = max(0, pet.fullness - amount)
pet = %{pet | fullness: new_fullness, changed: true}
if new_fullness == 0 do
# Pet loses closeness when starving
remove_closeness(pet, 1)
else
{:ok, pet}
end
end
@doc """
Sets the pet's fullness directly.
"""
def set_fullness(%__MODULE__{} = pet, fullness) do
%{pet | fullness: max(0, min(@max_fullness, fullness)), changed: true}
end
@doc """
Sets the pet's flags (abilities bitmask).
"""
def set_flags(%__MODULE__{} = pet, flags) do
%{pet | flags: flags, changed: true}
end
@doc """
Adds a flag to the pet's abilities.
"""
def add_flag(%__MODULE__{} = pet, flag) do
%{pet | flags: Bitwise.bor(pet.flags, flag), changed: true}
end
@doc """
Removes a flag from the pet's abilities.
"""
def remove_flag(%__MODULE__{} = pet, flag) do
%{pet | flags: Bitwise.band(pet.flags, Bitwise.bnot(flag)), changed: true}
end
@doc """
Checks if the pet has a specific flag.
"""
def has_flag?(%__MODULE__{} = pet, flag) do
Bitwise.band(pet.flags, flag) == flag
end
@doc """
Updates the pet's position.
"""
def update_position(%__MODULE__{} = pet, x, y, fh \\ nil, stance \\ nil) do
new_position = %{pet.position | x: x, y: y}
new_position = if fh, do: %{new_position | fh: fh}, else: new_position
pet = %{pet | position: new_position}
pet = if stance, do: %{pet | stance: stance}, else: pet
pet
end
@doc """
Sets the seconds left (for time-limited pets).
"""
def set_seconds_left(%__MODULE__{} = pet, seconds) do
%{pet | seconds_left: seconds, changed: true}
end
@doc """
Decreases seconds left for time-limited pets.
Returns {:expired, pet} if time runs out, {:ok, pet} otherwise.
"""
def tick_seconds(%__MODULE__{} = pet) do
if pet.seconds_left > 0 do
new_seconds = pet.seconds_left - 1
pet = %{pet | seconds_left: new_seconds, changed: true}
if new_seconds == 0 do
{:expired, pet}
else
{:ok, pet}
end
else
{:ok, pet}
end
end
@doc """
Marks the pet as saved (clears changed flag).
"""
def mark_saved(%__MODULE__{} = pet) do
%{pet | changed: false}
end
@doc """
Checks if the pet can consume a specific food item.
"""
def can_consume?(%__MODULE__{} = pet, item_id) do
# Different pets can eat different foods
# This would check against item data for valid pet foods
item_id >= 5_120_000 and item_id < 5_130_000
end
@doc """
Returns the pet's hunger rate (how fast fullness decreases).
Based on pet item ID.
"""
def get_hunger(%__MODULE__{} = pet) do
PetData.get_hunger(pet.pet_item_id)
end
@doc """
Gets the pet's progress to next level as a percentage.
"""
def level_progress(%__MODULE__{} = pet) do
current_req = PetData.closeness_for_level(pet.level)
next_req = PetData.closeness_for_level(pet.level + 1)
if next_req == current_req do
100
else
progress = pet.closeness - current_req
needed = next_req - current_req
trunc(progress / needed * 100)
end
end
@doc """
Converts pet to a map for database storage.
"""
def to_db_map(%__MODULE__{} = pet) do
%{
petid: pet.unique_id,
name: pet.name,
level: pet.level,
closeness: pet.closeness,
fullness: pet.fullness,
seconds: pet.seconds_left,
flags: pet.flags
}
end
end