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