Files
odinsea-elixir/lib/odinsea/game/inventory.ex
2026-02-14 19:36:59 -07:00

395 lines
9.9 KiB
Elixir

defmodule Odinsea.Game.Inventory do
@moduledoc """
Manages a single inventory type (equip, use, setup, etc, cash, equipped).
Provides operations for adding, removing, moving, and querying items.
"""
alias Odinsea.Game.{Item, Equip, InventoryType}
defstruct [
:type,
:slot_limit,
items: %{}
]
@type t :: %__MODULE__{
type: InventoryType.t(),
slot_limit: integer(),
items: map()
}
@doc """
Creates a new inventory of the specified type.
"""
def new(type, slot_limit \\ nil) do
limit = slot_limit || InventoryType.default_slot_limit(type)
%__MODULE__{
type: type,
slot_limit: limit,
items: %{}
}
end
@doc """
Gets the slot limit of the inventory.
"""
def slot_limit(%__MODULE__{} = inv), do: inv.slot_limit
@doc """
Sets the slot limit (max 96).
"""
def set_slot_limit(%__MODULE__{} = inv, limit) when limit > 96 do
%{inv | slot_limit: 96}
end
def set_slot_limit(%__MODULE__{} = inv, limit) do
%{inv | slot_limit: limit}
end
@doc """
Adds slots to the inventory limit.
"""
def add_slots(%__MODULE__{} = inv, slots) do
new_limit = min(inv.slot_limit + slots, 96)
%{inv | slot_limit: new_limit}
end
@doc """
Finds an item by its item_id.
Returns the first matching item or nil.
"""
def find_by_id(%__MODULE__{} = inv, item_id) do
inv.items
|> Map.values()
|> Enum.find(fn item -> item.item_id == item_id end)
end
@doc """
Finds an item by its unique_id.
"""
def find_by_unique_id(%__MODULE__{} = inv, unique_id) do
inv.items
|> Map.values()
|> Enum.find(fn item -> item.unique_id == unique_id end)
end
@doc """
Finds an item by both inventory_id and item_id.
"""
def find_by_inventory_id(%__MODULE__{} = inv, inventory_id, item_id) do
inv.items
|> Map.values()
|> Enum.find(fn item ->
item.inventory_id == inventory_id && item.item_id == item_id
end) || find_by_id(inv, item_id)
end
@doc """
Gets the total count of an item by item_id across all slots.
"""
def count_by_id(%__MODULE__{} = inv, item_id) do
inv.items
|> Map.values()
|> Enum.filter(fn item -> item.item_id == item_id end)
|> Enum.map(fn item -> item.quantity end)
|> Enum.sum()
end
@doc """
Lists all items with the given item_id.
"""
def list_by_id(%__MODULE__{} = inv, item_id) do
items =
inv.items
|> Map.values()
|> Enum.filter(fn item -> item.item_id == item_id end)
|> Enum.sort(&Item.compare/2)
items
end
@doc """
Lists all items in the inventory.
"""
def list(%__MODULE__{} = inv) do
Map.values(inv.items)
end
@doc """
Lists all unique item IDs in the inventory.
"""
def list_ids(%__MODULE__{} = inv) do
inv.items
|> Map.values()
|> Enum.map(fn item -> item.item_id end)
|> Enum.uniq()
|> Enum.sort()
end
@doc """
Gets an item at a specific position.
"""
def get_item(%__MODULE__{} = inv, position) do
Map.get(inv.items, position)
end
@doc """
Checks if the inventory is full.
"""
def full?(%__MODULE__{} = inv) do
map_size(inv.items) >= inv.slot_limit
end
@doc """
Checks if the inventory would be full with additional items.
"""
def full?(%__MODULE__{} = inv, margin) do
map_size(inv.items) + margin >= inv.slot_limit
end
@doc """
Gets the number of free slots.
"""
def free_slots(%__MODULE__{} = inv) do
inv.slot_limit - map_size(inv.items)
end
@doc """
Gets the next available slot number.
Returns -1 if the inventory is full.
"""
def next_free_slot(%__MODULE__{} = inv) do
if full?(inv) do
-1
else
find_next_slot(inv.items, inv.slot_limit, 1)
end
end
defp find_next_slot(items, limit, slot) when slot > limit, do: -1
defp find_next_slot(items, limit, slot) do
if Map.has_key?(items, slot) do
find_next_slot(items, limit, slot + 1)
else
slot
end
end
@doc """
Adds an item to the inventory.
Returns {:ok, position, inventory} or {:error, :inventory_full}.
"""
def add_item(%__MODULE__{} = inv, %Item{} = item) do
slot = next_free_slot(inv)
if slot < 0 do
{:error, :inventory_full}
else
item = %{item | position: slot}
new_items = Map.put(inv.items, slot, item)
{:ok, slot, %{inv | items: new_items}}
end
end
def add_item(%__MODULE__{} = inv, %Equip{} = equip) do
slot = next_free_slot(inv)
if slot < 0 do
{:error, :inventory_full}
else
equip = %{equip | position: slot}
new_items = Map.put(inv.items, slot, equip)
{:ok, slot, %{inv | items: new_items}}
end
end
@doc """
Adds an item from the database (preserves position).
"""
def add_from_db(%__MODULE__{} = inv, %{position: position} = item) do
# Validate position for equipped vs unequipped
valid_position = validate_position(inv.type, position)
if valid_position do
new_items = Map.put(inv.items, position, item)
%{inv | items: new_items}
else
inv
end
end
defp validate_position(:equipped, position) when position < 0, do: true
defp validate_position(:equipped, _position), do: false
defp validate_position(_type, position) when position > 0, do: true
defp validate_position(_type, _position), do: false
@doc """
Removes an item from a specific slot.
"""
def remove_slot(%__MODULE__{} = inv, position) do
new_items = Map.delete(inv.items, position)
%{inv | items: new_items}
end
@doc """
Removes a quantity from an item.
If quantity reaches 0, removes the item entirely (unless allow_zero is true).
"""
def remove_item(%__MODULE__{} = inv, position, quantity \\ 1, allow_zero \\ false) do
case Map.get(inv.items, position) do
nil ->
inv
item ->
new_qty = item.quantity - quantity
new_qty = if new_qty < 0, do: 0, else: new_qty
if new_qty == 0 and not allow_zero do
remove_slot(inv, position)
else
new_item = %{item | quantity: new_qty}
new_items = Map.put(inv.items, position, new_item)
%{inv | items: new_items}
end
end
end
@doc """
Moves an item from one slot to another.
Handles equipping/unequipping and stacking.
"""
def move(%__MODULE__{} = inv, src_slot, dst_slot, slot_max \\ 100) do
source = Map.get(inv.items, src_slot)
target = Map.get(inv.items, dst_slot)
cond do
is_nil(source) ->
{:error, :empty_source}
is_nil(target) ->
# Simple move to empty slot
if valid_position?(inv.type, dst_slot) do
new_items =
inv.items
|> Map.delete(src_slot)
|> Map.put(dst_slot, %{source | position: dst_slot})
{:ok, %{inv | items: new_items}}
else
{:error, :invalid_position}
end
can_stack?(source, target, inv.type) ->
# Stack items
stack_items(inv, src_slot, dst_slot, source, target, slot_max)
true ->
# Swap items
{:ok, swap_items(inv, src_slot, dst_slot, source, target)}
end
end
defp valid_position?(:equipped, position) when position < 0, do: true
defp valid_position?(:equipped, _), do: false
defp valid_position?(_, position) when position > 0, do: true
defp valid_position?(_, _), do: false
defp can_stack?(source, target, type) do
# Can't stack equipment or cash items
not InventoryType.equipment?(type) and
type != :cash and
source.item_id == target.item_id and
source.owner == target.owner and
source.expiration == target.expiration and
source.flag == target.flag
end
defp stack_items(inv, src_slot, dst_slot, source, target, slot_max) do
total_qty = source.quantity + target.quantity
if total_qty > slot_max do
# Partial stack
new_source = %{source | quantity: total_qty - slot_max}
new_target = %{target | quantity: slot_max}
new_items =
inv.items
|> Map.put(src_slot, new_source)
|> Map.put(dst_slot, new_target)
{:ok, %{inv | items: new_items}}
else
# Full stack - remove source
new_target = %{target | quantity: total_qty}
new_items =
inv.items
|> Map.delete(src_slot)
|> Map.put(dst_slot, new_target)
{:ok, %{inv | items: new_items}}
end
end
defp swap_items(inv, src_slot, dst_slot, source, target) do
new_source = %{source | position: dst_slot}
new_target = %{target | position: src_slot}
inv.items
|> Map.put(dst_slot, new_source)
|> Map.put(src_slot, new_target)
end
@doc """
Drops an item from the inventory.
Returns {:ok, dropped_item, updated_inventory} or {:error, reason}.
"""
def drop(%__MODULE__{} = inv, position, quantity \\ 1) do
case Map.get(inv.items, position) do
nil ->
{:error, :item_not_found}
item ->
if quantity >= item.quantity do
# Drop entire stack
new_items = Map.delete(inv.items, position)
{:ok, item, %{inv | items: new_items}}
else
# Drop partial stack
new_item = %{item | quantity: item.quantity - quantity}
dropped_item = %{item | quantity: quantity}
new_items = Map.put(inv.items, position, new_item)
{:ok, dropped_item, %{inv | items: new_items}}
end
end
end
@doc """
Updates an item at a specific position.
"""
def update_item(%__MODULE__{} = inv, position, updater_fn) when is_function(updater_fn, 1) do
case Map.get(inv.items, position) do
nil ->
inv
item ->
new_item = updater_fn.(item)
new_items = Map.put(inv.items, position, new_item)
%{inv | items: new_items}
end
end
@doc """
Gets all equipped items.
"""
def equipped_items(%__MODULE__{type: :equipped} = inv) do
inv.items
|> Map.values()
|> Enum.sort_by(fn item -> abs(item.position) end)
end
def equipped_items(%__MODULE__{}), do: []
end