defmodule Odinsea.Game.DropTable do @moduledoc """ Manages drop tables for monsters. Ported from Java server.life.MonsterDropEntry and MapleMonsterInformationProvider Drop tables define what items a monster can drop when killed. Each drop entry includes: - item_id: The item to drop - chance: Drop rate (out of 1,000,000 for most items) - min_quantity: Minimum quantity - max_quantity: Maximum quantity - quest_id: Quest requirement (-1 = no quest) """ alias Odinsea.Game.LifeFactory require Logger @typedoc "A single drop entry" @type drop_entry :: %{ item_id: integer(), chance: integer(), min_quantity: integer(), max_quantity: integer(), quest_id: integer() } @typedoc "Drop table for a monster" @type drop_table :: [drop_entry()] # Default drop rates for different item categories @equip_drop_rate 0.05 # 5% base for equipment @use_drop_rate 0.10 # 10% for use items @etc_drop_rate 0.15 # 15% for etc items @setup_drop_rate 0.02 # 2% for setup items @cash_drop_rate 0.01 # 1% for cash items # Meso drop configuration @meso_chance_normal 400_000 # 40% base for normal mobs @meso_chance_boss 1_000_000 # 100% for bosses @doc """ Gets the drop table for a monster. """ @spec get_drops(integer()) :: drop_table() def get_drops(mob_id) do # Try to get from cache first case lookup_drop_table(mob_id) do nil -> load_and_cache_drops(mob_id) drops -> drops end end @doc """ Calculates drops for a monster kill. Returns a list of {item_id, quantity} tuples or {:meso, amount}. """ @spec calculate_drops(integer(), integer(), map()) :: [{:item | :meso, integer(), integer()}] def calculate_drops(mob_id, drop_rate_multiplier, _opts \\ %{}) do drops = get_drops(mob_id) # Get monster stats for meso calculation stats = LifeFactory.get_monster_stats(mob_id) results = if stats do # Check if meso drops are disabled for this monster type should_drop_mesos = should_drop_mesos?(stats) # Add meso drops if applicable meso_drops = if should_drop_mesos do calculate_meso_drops(stats, drop_rate_multiplier) else [] end # Roll for each item drop item_drops = Enum.flat_map(drops, fn entry -> case roll_drop(entry, drop_rate_multiplier) do nil -> [] {item_id, quantity} -> [{:item, item_id, quantity}] end end) meso_drops ++ item_drops else [] end # Limit total drops to prevent flooding Enum.take(results, 10) end @doc """ Rolls for a single drop entry. Returns {item_id, quantity} if successful, nil if failed. """ @spec roll_drop(drop_entry(), integer() | float()) :: {integer(), integer()} | nil def roll_drop(entry, multiplier) do # Calculate adjusted chance base_chance = entry.chance adjusted_chance = trunc(base_chance * multiplier) # Roll (1,000,000 = 100%) roll = :rand.uniform(1_000_000) if roll <= adjusted_chance do # Determine quantity quantity = if entry.max_quantity > entry.min_quantity do entry.min_quantity + :rand.uniform(entry.max_quantity - entry.min_quantity + 1) - 1 else entry.min_quantity end {entry.item_id, max(1, quantity)} else nil end end @doc """ Calculates meso drops for a monster. """ @spec calculate_meso_drops(map(), integer() | float()) :: [{:meso, integer(), integer()}] def calculate_meso_drops(stats, drop_rate_multiplier) do # Determine number of meso drops based on monster type num_drops = cond do stats.boss and not stats.party_bonus -> 2 stats.party_bonus -> 1 true -> 1 end # Calculate max meso amount level = stats.level # Formula: level * (level / 10) = max # Min = 0.66 * max divided = if level < 100, do: max(level, 10) / 10.0, else: level / 10.0 max_amount = if stats.boss and not stats.party_bonus do level * level else trunc(level * :math.ceil(level / divided)) end min_amount = trunc(0.66 * max_amount) # Roll for each meso drop base_chance = if stats.boss and not stats.party_bonus, do: @meso_chance_boss, else: @meso_chance_normal adjusted_chance = trunc(base_chance * drop_rate_multiplier) Enum.flat_map(1..num_drops, fn _ -> roll = :rand.uniform(1_000_000) if roll <= adjusted_chance do amount = min_amount + :rand.uniform(max(1, max_amount - min_amount + 1)) - 1 [{:meso, max(1, amount), 1}] else [] end end) end @doc """ Gets global drops that apply to all monsters. """ @spec get_global_drops() :: drop_table() def get_global_drops do # Global drops from database (would be loaded from drop_data_global table) # For now, return empty list [] end @doc """ Clears the drop table cache. """ def clear_cache do :ets.delete_all_objects(:drop_table_cache) end # ============================================================================ # Private Functions # ============================================================================ defp lookup_drop_table(mob_id) do case :ets.lookup(:drop_table_cache, mob_id) do [{^mob_id, drops}] -> drops [] -> nil end end defp load_and_cache_drops(mob_id) do drops = load_drops_from_source(mob_id) :ets.insert(:drop_table_cache, {mob_id, drops}) drops end defp load_drops_from_source(mob_id) do # In a full implementation, this would: # 1. Query drop_data_final_v2 table # 2. Apply chance adjustments based on item type # 3. Return processed drops # For now, return fallback drops based on monster level generate_fallback_drops(mob_id) end defp generate_fallback_drops(mob_id) do # Get monster stats to determine level-appropriate drops case LifeFactory.get_monster_stats(mob_id) do nil -> [] stats -> generate_level_drops(stats) end end defp generate_level_drops(stats) do level = stats.level # Generate appropriate drops based on monster level cond do level <= 10 -> # Beginner drops [ %{item_id: 2000000, chance: 50_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # Red Potion %{item_id: 2000001, chance: 50_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # Orange Potion %{item_id: 4000000, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Snail Shell %{item_id: 4000001, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Blue Snail Shell ] level <= 20 -> # Low level drops [ %{item_id: 2000002, chance: 50_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # White Potion %{item_id: 2000003, chance: 50_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # Blue Potion %{item_id: 4000002, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Red Snail Shell %{item_id: 4000010, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Mushroom Spores ] level <= 40 -> # Mid level drops [ %{item_id: 2000004, chance: 40_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # Elixir %{item_id: 2000005, chance: 40_000, min_quantity: 1, max_quantity: 3, quest_id: -1}, # Power Elixir %{item_id: 4000011, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Pig's Head %{item_id: 4000012, chance: 30_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Pig's Ribbon ] level <= 70 -> # Higher level drops [ %{item_id: 2000005, chance: 50_000, min_quantity: 1, max_quantity: 5, quest_id: -1}, # Power Elixir %{item_id: 2040000, chance: 10_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Scroll for Helmet %{item_id: 2040800, chance: 10_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Scroll for Gloves ] true -> # High level drops [ %{item_id: 2000005, chance: 60_000, min_quantity: 1, max_quantity: 10, quest_id: -1}, # Power Elixir %{item_id: 2044000, chance: 5_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Scroll for Two-Handed Sword %{item_id: 2044100, chance: 5_000, min_quantity: 1, max_quantity: 1, quest_id: -1}, # Scroll for Two-Handed Axe ] end end defp should_drop_mesos?(stats) do # Don't drop mesos if: # - Monster has special properties (invincible, friendly, etc.) # - Monster is a fixed damage mob # - Monster is a special event mob cond do stats.invincible -> false stats.friendly -> false stats.fixed_damage > 0 -> false stats.remove_after > 0 -> false true -> true end end @doc """ Initializes the drop table cache ETS table. Called during application startup. """ def init_cache do :ets.new(:drop_table_cache, [ :set, :public, :named_table, read_concurrency: true, write_concurrency: true ]) Logger.info("Drop table cache initialized") :ok end # ============================================================================ # GenServer Implementation (for supervision) # ============================================================================ use GenServer @doc """ Starts the DropTable cache manager. """ def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @impl true def init(_) do init_cache() {:ok, %{}} end end