333 lines
8.1 KiB
Elixir
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
|