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