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