322 lines
9.8 KiB
Elixir
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
|