281 lines
8.5 KiB
Elixir
281 lines
8.5 KiB
Elixir
defmodule Odinsea.Game.DropSystem do
|
|
@moduledoc """
|
|
Drop creation and management system.
|
|
Ported from Java server.maps.MapleMap.spawnMobDrop()
|
|
|
|
This module handles:
|
|
- Creating drops when monsters die
|
|
- Calculating drop positions
|
|
- Managing drop ownership
|
|
- Drop expiration
|
|
"""
|
|
|
|
alias Odinsea.Game.{Drop, DropTable, Item, ItemInfo}
|
|
alias Odinsea.Game.LifeFactory
|
|
|
|
require Logger
|
|
|
|
# Default drop rate multiplier
|
|
@default_drop_rate 1.0
|
|
|
|
# Drop type constants
|
|
@drop_type_owner_timeout 0 # Timeout for non-owner
|
|
@drop_type_party_timeout 1 # Timeout for non-owner's party
|
|
@drop_type_ffa 2 # Free for all
|
|
@drop_type_explosive 3 # Explosive (instant FFA)
|
|
|
|
@doc """
|
|
Creates drops for a killed monster.
|
|
Returns a list of Drop structs.
|
|
"""
|
|
@spec create_monster_drops(integer(), integer(), map(), integer(), float()) :: [Drop.t()]
|
|
def create_monster_drops(mob_id, owner_id, position, next_oid, drop_rate_multiplier \\ @default_drop_rate) do
|
|
# Get monster stats
|
|
stats = LifeFactory.get_monster_stats(mob_id)
|
|
|
|
if stats == nil do
|
|
Logger.warning("Cannot create drops - monster stats not found for mob_id #{mob_id}")
|
|
[]
|
|
else
|
|
# Calculate what should drop
|
|
calculated_drops = DropTable.calculate_drops(mob_id, drop_rate_multiplier)
|
|
|
|
# Create Drop structs
|
|
{drops, _next_oid} =
|
|
Enum.reduce(calculated_drops, {[], next_oid}, fn drop_data, {acc, oid} ->
|
|
case create_drop(drop_data, owner_id, position, oid, stats) do
|
|
nil -> {acc, oid}
|
|
drop -> {[drop | acc], oid + 1}
|
|
end
|
|
end)
|
|
|
|
Enum.reverse(drops)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a single drop from calculated drop data.
|
|
"""
|
|
@spec create_drop({atom(), integer(), integer()}, integer(), map(), integer(), map()) :: Drop.t() | nil
|
|
def create_drop({:meso, amount, _}, owner_id, position, oid, stats) do
|
|
# Calculate drop position (small random offset from monster)
|
|
drop_position = calculate_drop_position(position)
|
|
|
|
# Determine drop type based on monster
|
|
drop_type =
|
|
cond do
|
|
stats.boss and not stats.party_bonus -> @drop_type_explosive
|
|
stats.party_bonus -> @drop_type_party_timeout
|
|
true -> @drop_type_ffa
|
|
end
|
|
|
|
Drop.new_meso_drop(oid, amount, owner_id, drop_position,
|
|
drop_type: drop_type,
|
|
dropper_oid: nil,
|
|
source_position: position
|
|
)
|
|
end
|
|
|
|
def create_drop({:item, item_id, quantity}, owner_id, position, oid, stats) do
|
|
# Validate item exists
|
|
if ItemInfo.item_exists?(item_id) do
|
|
# Calculate drop position
|
|
drop_position = calculate_drop_position(position)
|
|
|
|
# Determine drop type
|
|
drop_type =
|
|
cond do
|
|
stats.boss and not stats.party_bonus -> @drop_type_explosive
|
|
stats.party_bonus -> @drop_type_party_timeout
|
|
true -> @drop_type_ffa
|
|
end
|
|
|
|
Drop.new_item_drop(oid, item_id, quantity, owner_id, drop_position,
|
|
drop_type: drop_type,
|
|
dropper_oid: nil,
|
|
source_position: position
|
|
)
|
|
else
|
|
Logger.debug("Item #{item_id} not found, skipping drop")
|
|
nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates an item drop from a player's inventory.
|
|
Used when a player drops an item.
|
|
"""
|
|
@spec create_player_drop(Item.t(), integer(), map(), integer()) :: Drop.t()
|
|
def create_player_drop(item, owner_id, position, oid) do
|
|
drop_position = calculate_drop_position(position)
|
|
|
|
Drop.new_item_drop(oid, item.item_id, item.quantity, owner_id, drop_position,
|
|
drop_type: @drop_type_ffa, # Player drops are always FFA
|
|
player_drop: true
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Creates an equipment drop with randomized stats.
|
|
"""
|
|
@spec create_equipment_drop(integer(), integer(), map(), integer(), float()) :: Drop.t() | nil
|
|
def create_equipment_drop(item_id, owner_id, position, oid, _drop_rate_multiplier \\ 1.0) do
|
|
# Check if item is equipment
|
|
case ItemInfo.get_inventory_type(item_id) do
|
|
:equip ->
|
|
# Get equipment stats
|
|
equip = ItemInfo.create_equip(item_id)
|
|
|
|
if equip do
|
|
drop_position = calculate_drop_position(position)
|
|
|
|
Drop.new_item_drop(oid, item_id, 1, owner_id, drop_position,
|
|
drop_type: @drop_type_ffa
|
|
)
|
|
else
|
|
nil
|
|
end
|
|
|
|
_ ->
|
|
# Not equipment, create regular item drop
|
|
create_drop({:item, item_id, 1}, owner_id, position, oid, %{})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Calculates a drop position near the source position.
|
|
"""
|
|
@spec calculate_drop_position(map()) :: %{x: integer(), y: integer()}
|
|
def calculate_drop_position(%{x: x, y: y}) do
|
|
# Random offset within range
|
|
offset_x = :rand.uniform(80) - 40 # -40 to +40
|
|
offset_y = :rand.uniform(20) - 10 # -10 to +10
|
|
|
|
%{
|
|
x: x + offset_x,
|
|
y: y + offset_y
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Checks and handles drop expiration.
|
|
Returns updated drops list with expired ones marked.
|
|
"""
|
|
@spec check_expiration([Drop.t()], integer()) :: [Drop.t()]
|
|
def check_expiration(drops, now) do
|
|
Enum.map(drops, fn drop ->
|
|
if Drop.should_expire?(drop, now) do
|
|
Drop.mark_picked_up(drop)
|
|
else
|
|
drop
|
|
end
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Filters out expired and picked up drops.
|
|
"""
|
|
@spec cleanup_drops([Drop.t()]) :: [Drop.t()]
|
|
def cleanup_drops(drops) do
|
|
Enum.reject(drops, fn drop ->
|
|
drop.picked_up
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Attempts to pick up a drop.
|
|
Returns {:ok, drop} if successful, {:error, reason} if not.
|
|
"""
|
|
@spec pickup_drop(Drop.t(), integer(), integer()) :: {:ok, Drop.t()} | {:error, atom()}
|
|
def pickup_drop(drop, character_id, now) do
|
|
cond do
|
|
drop.picked_up ->
|
|
{:error, :already_picked_up}
|
|
|
|
not Drop.can_loot?(drop, character_id, now) ->
|
|
{:error, :not_owner}
|
|
|
|
true ->
|
|
{:ok, Drop.mark_picked_up(drop)}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all visible drops for a character.
|
|
"""
|
|
@spec get_visible_drops([Drop.t()], integer(), map()) :: [Drop.t()]
|
|
def get_visible_drops(drops, character_id, quest_status) do
|
|
Enum.filter(drops, fn drop ->
|
|
Drop.visible_to?(drop, character_id, quest_status)
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Determines drop ownership type based on damage contribution.
|
|
"""
|
|
@spec determine_drop_type([{integer(), integer()}], integer()) :: integer()
|
|
def determine_drop_type(attackers, _killer_id) do
|
|
# If only one attacker, owner-only
|
|
# If multiple attackers, party/FFA based on damage distribution
|
|
|
|
case length(attackers) do
|
|
1 -> @drop_type_owner_timeout
|
|
_ -> @drop_type_party_timeout
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates drops with specific ownership rules.
|
|
Used for special drops like event rewards.
|
|
"""
|
|
@spec create_special_drop(integer(), integer(), integer(), map(), integer(), keyword()) :: Drop.t()
|
|
def create_special_drop(item_id, quantity, owner_id, position, oid, opts \\ []) do
|
|
drop_type = Keyword.get(opts, :drop_type, @drop_type_explosive)
|
|
quest_id = Keyword.get(opts, :quest_id, -1)
|
|
individual = Keyword.get(opts, :individual_reward, false)
|
|
|
|
Drop.new_item_drop(oid, item_id, quantity, owner_id, position,
|
|
drop_type: drop_type,
|
|
quest_id: quest_id,
|
|
individual_reward: individual
|
|
)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Global Drop System (Global Drops apply to all monsters)
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Creates global drops for a monster kill.
|
|
These are additional drops that can drop from any monster.
|
|
"""
|
|
@spec create_global_drops(integer(), map(), integer(), float()) :: [Drop.t()]
|
|
def create_global_drops(owner_id, position, next_oid, drop_rate_multiplier \\ @default_drop_rate) do
|
|
global_entries = DropTable.get_global_drops()
|
|
|
|
{drops, _next_oid} =
|
|
Enum.reduce(global_entries, {[], next_oid}, fn entry, {acc, oid} ->
|
|
case DropTable.roll_drop(entry, drop_rate_multiplier) do
|
|
nil ->
|
|
{acc, oid}
|
|
|
|
{item_id, quantity} ->
|
|
if ItemInfo.item_exists?(item_id) do
|
|
drop_position = calculate_drop_position(position)
|
|
|
|
drop = Drop.new_item_drop(oid, item_id, quantity, owner_id, drop_position,
|
|
drop_type: entry.drop_type,
|
|
quest_id: entry.questid
|
|
)
|
|
|
|
{[drop | acc], oid + 1}
|
|
else
|
|
{acc, oid}
|
|
end
|
|
end
|
|
end)
|
|
|
|
Enum.reverse(drops)
|
|
end
|
|
end
|