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