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

322 lines
9.8 KiB
Elixir

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