kimi gone wild
This commit is contained in:
321
lib/odinsea/game/drop_table.ex
Normal file
321
lib/odinsea/game/drop_table.ex
Normal file
@@ -0,0 +1,321 @@
|
||||
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
|
||||
Reference in New Issue
Block a user