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