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

187 lines
5.7 KiB
Elixir

defmodule Odinsea.Shop.CashItem do
@moduledoc """
Cash Shop Item struct and utilities.
Represents an item available for purchase in the Cash Shop.
Ported from server/CashItemInfo.java and server/cash/CashCommodity.java
"""
@type t :: %__MODULE__{
sn: integer(),
item_id: integer(),
price: integer(),
count: integer(),
period: integer(),
gender: integer(),
on_sale: boolean(),
class: integer(),
priority: integer(),
is_package: boolean(),
meso_price: integer(),
bonus: integer(),
for_premium_user: integer(),
limit: integer(),
extra_flags: integer()
}
defstruct [
:sn,
:item_id,
:price,
:count,
:period,
:gender,
:on_sale,
:class,
:priority,
:is_package,
:meso_price,
:bonus,
:for_premium_user,
:limit,
:extra_flags
]
@doc """
Creates a new CashItem struct from parsed data.
"""
@spec new(map()) :: t()
def new(attrs) do
%__MODULE__{
sn: Map.get(attrs, :sn, 0),
item_id: Map.get(attrs, :item_id, 0),
price: Map.get(attrs, :price, 0),
count: Map.get(attrs, :count, 1),
period: Map.get(attrs, :period, 0),
gender: Map.get(attrs, :gender, 2),
on_sale: Map.get(attrs, :on_sale, false),
class: Map.get(attrs, :class, 0),
priority: Map.get(attrs, :priority, 0),
is_package: Map.get(attrs, :is_package, false),
meso_price: Map.get(attrs, :meso_price, 0),
bonus: Map.get(attrs, :bonus, 0),
for_premium_user: Map.get(attrs, :for_premium_user, 0),
limit: Map.get(attrs, :limit, 0),
extra_flags: Map.get(attrs, :extra_flags, 0)
}
end
@doc """
Checks if the item gender matches the player's gender.
Gender: 0 = male, 1 = female, 2 = both
"""
@spec gender_matches?(t(), integer()) :: boolean()
def gender_matches?(%__MODULE__{gender: 2}, _player_gender), do: true
def gender_matches?(%__MODULE__{gender: gender}, player_gender), do: gender == player_gender
@doc """
Calculates the flags value for packet encoding.
This follows the Java CashCommodity flag calculation.
"""
@spec calculate_flags(t()) :: integer()
def calculate_flags(item) do
flags = item.extra_flags || 0
flags = if item.item_id > 0, do: Bitwise.bor(flags, 0x1), else: flags
flags = if item.count > 0, do: Bitwise.bor(flags, 0x2), else: flags
flags = if item.price > 0, do: Bitwise.bor(flags, 0x4), else: flags
flags = if item.bonus > 0, do: Bitwise.bor(flags, 0x8), else: flags
flags = if item.priority >= 0, do: Bitwise.bor(flags, 0x10), else: flags
flags = if item.period > 0, do: Bitwise.bor(flags, 0x20), else: flags
# 0x40 = nMaplePoint (not used)
flags = if item.meso_price > 0, do: Bitwise.bor(flags, 0x80), else: flags
flags = if item.for_premium_user > 0, do: Bitwise.bor(flags, 0x100), else: flags
flags = if item.gender >= 0, do: Bitwise.bor(flags, 0x200), else: flags
flags = if item.on_sale, do: Bitwise.bor(flags, 0x400), else: flags
flags = if item.class >= -1 && item.class <= 3, do: Bitwise.bor(flags, 0x800), else: flags
flags = if item.limit > 0, do: Bitwise.bor(flags, 0x1000), else: flags
# 0x2000, 0x4000, 0x8000 = nPbCash, nPbPoint, nPbGift (not used)
flags = if item.is_package, do: Bitwise.bor(flags, 0x40000), else: flags
# 0x80000, 0x100000 = term start/end (not used)
flags
end
@doc """
Checks if this is a cash item (premium currency item).
"""
@spec cash_item?(integer()) :: boolean()
def cash_item?(item_id) do
# Cash items typically have IDs in certain ranges
# This is a simplified check - full implementation would check WZ data
item_id >= 500_000 && item_id < 600_000
end
@doc """
Checks if this item is a pet.
"""
@spec pet?(t()) :: boolean()
def pet?(%__MODULE__{item_id: item_id}) do
item_id >= 5_000_000 && item_id < 5_100_000
end
@doc """
Checks if this is a permanent pet.
"""
@spec permanent_pet?(t()) :: boolean()
def permanent_pet?(%__MODULE__{item_id: item_id}) do
item_id >= 5_000_100 && item_id < 5_000_200
end
@doc """
Gets the effective period for this item.
Returns period in days, or special values for permanent items.
"""
@spec effective_period(t()) :: integer()
def effective_period(%__MODULE__{period: period} = item) do
cond do
# Permanent pets have special handling
permanent_pet?(item) -> 20_000
# Default period for non-equip cash items that aren't permanent
period <= 0 && !equip_item?(item) -> 90
true -> period
end
end
@doc """
Checks if this item is equipment.
"""
@spec equip_item?(t()) :: boolean()
def equip_item?(%__MODULE__{item_id: item_id}) do
item_id >= 1_000_000
end
@doc """
Calculates the expiration timestamp for this item.
"""
@spec expiration_time(t()) :: integer()
def expiration_time(item) do
period = effective_period(item)
if period > 0 do
Odinsea.now() + period * 24 * 60 * 60 * 1000
else
-1
end
end
@doc """
Applies modified item info (from cashshop_modified_items table).
"""
@spec apply_mods(t(), map()) :: t()
def apply_mods(item, mods) do
%__MODULE__{
item
| item_id: Map.get(mods, :item_id, item.item_id),
price: Map.get(mods, :price, item.price),
count: Map.get(mods, :count, item.count),
period: Map.get(mods, :period, item.period),
gender: Map.get(mods, :gender, item.gender),
on_sale: Map.get(mods, :on_sale, item.on_sale),
class: Map.get(mods, :class, item.class),
priority: Map.get(mods, :priority, item.priority),
is_package: Map.get(mods, :is_package, item.is_package)
}
end
end