defmodule Odinsea.Channel.Handler.ItemMaker do @moduledoc """ Handles item crafting/making operations. Ported from: src/handling/channel/handler/ItemMakerHandler.java ## Maker Types - 1: Create items/gems/equipment - 3: Make crystals from etc items - 4: Disassemble equipment ## Profession Skills - 92000000: Herbalism - 92010000: Mining - 92020000: Smithing - 92030000: Accessory Crafting - 92040000: Alchemy ## Main Handlers - handle_item_maker/2 - Item crafting - handle_use_recipe/2 - Recipe usage - handle_make_extractor/2 - Extractor creation - handle_use_bag/2 - Herb/Mining bag usage - handle_start_harvest/2 - Start harvesting - handle_stop_harvest/2 - Stop harvesting - handle_profession_info/2 - Profession info request - handle_craft_effect/2 - Crafting animation effect - handle_craft_make/2 - Crafting make animation - handle_craft_complete/2 - Crafting completion - handle_use_pot/2 - Item pot usage - handle_clear_pot/2 - Clear item pot - handle_feed_pot/2 - Feed item pot - handle_cure_pot/2 - Cure item pot - handle_reward_pot/2 - Reward from item pot """ require Logger alias Odinsea.Net.Packet.In alias Odinsea.Game.{Character, Inventory} alias Odinsea.Channel.Packets # Crafting effect mapping @crafting_effects %{ "Effect/BasicEff.img/professions/herbalism" => 92000000, "Effect/BasicEff.img/professions/mining" => 92010000, "Effect/BasicEff.img/professions/herbalismExtract" => 92000000, "Effect/BasicEff.img/professions/miningExtract" => 92010000, "Effect/BasicEff.img/professions/equip_product" => 92020000, "Effect/BasicEff.img/professions/acc_product" => 92030000, "Effect/BasicEff.img/professions/alchemy" => 92040000 } # ============================================================================ # Item Making # ============================================================================ @doc """ Handles item maker operations (CP_ITEM_MAKER / 0x87). Maker types: - 1: Gem creation, other gem creation, or equipment making - 3: Crystal making from etc items - 4: Equipment disassembly Reference: ItemMakerHandler.ItemMaker() """ def handle_item_maker(packet, client_pid) do maker_type = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> case maker_type do 1 -> handle_make_item(packet, client_pid, character_id, char_state) 3 -> handle_make_crystal(packet, client_pid, character_id, char_state) 4 -> handle_disassemble(packet, client_pid, character_id, char_state) _ -> Logger.warning("Unknown maker type: #{maker_type}, character #{character_id}") :ok end {:error, reason} -> Logger.warn("Failed to handle item maker: #{inspect(reason)}") end end # Handle type 1: Make item/gem/equipment defp handle_make_item(packet, client_pid, character_id, char_state) do to_create = In.decode_int(packet) # Check what type of creation this is cond do is_gem?(to_create) -> # Gem creation with random reward handle_gem_creation(packet, client_pid, character_id, char_state, to_create) is_other_gem?(to_create) -> # Non-gem items created with gem recipe handle_other_gem_creation(packet, client_pid, character_id, char_state, to_create) true -> # Equipment creation handle_equip_creation(packet, client_pid, character_id, char_state, to_create) end end defp handle_gem_creation(packet, client_pid, character_id, char_state, item_id) do # TODO: Get gem info from ItemMakerFactory # gem = ItemMakerFactory.get_gem_info(item_id) # TODO: Check skill level # TODO: Check meso cost # TODO: Check inventory space # TODO: Remove required items # TODO: Give random gem reward Logger.debug("Gem creation: item #{item_id}, character #{character_id}") # Send success packet send(client_pid, {:send_packet, Packets.enable_actions()}) :ok end defp handle_other_gem_creation(packet, client_pid, character_id, char_state, item_id) do # TODO: Similar to gem creation but with fixed reward Logger.debug("Other gem creation: item #{item_id}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok end defp handle_equip_creation(packet, client_pid, character_id, char_state, item_id) do stimulator = In.decode_byte(packet) > 0 num_enchanter = In.decode_int(packet) # TODO: Get creation info from ItemMakerFactory # create = ItemMakerFactory.get_create_info(item_id) # TODO: Validate enchanter count <= TUC # TODO: Check skill level # TODO: Check meso cost # TODO: Check inventory space # TODO: Remove required items # TODO: Create equipment with optional stimulator/enchanters Logger.debug("Equip creation: item #{item_id}, stimulator=#{stimulator}, enchanters=#{num_enchanter}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok end # Handle type 3: Make crystals defp handle_make_crystal(packet, client_pid, character_id, char_state) do etc_id = In.decode_int(packet) # TODO: Validate player has 100 of the etc item # TODO: Get crystal ID based on item level # crystal_id = get_create_crystal(etc_id) # TODO: Add crystal to inventory # TODO: Remove etc items Logger.debug("Crystal creation: etc #{etc_id}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok end # Handle type 4: Disassemble equipment defp handle_disassemble(packet, client_pid, character_id, char_state) do item_id = In.decode_int(packet) tick = In.decode_int(packet) slot = In.decode_int(packet) # TODO: Validate item exists in equip inventory # TODO: Get item level # TODO: Calculate crystal reward # TODO: Add crystals to inventory # TODO: Remove equipment Logger.debug("Disassemble: item #{item_id} at slot #{slot}, tick #{tick}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok end # ============================================================================ # Recipe Usage # ============================================================================ @doc """ Handles recipe usage (CP_USE_RECIPE / 0x5A). Recipes are items that teach crafting skills. Reference: ItemMakerHandler.UseRecipe() """ def handle_use_recipe(packet, client_pid) do tick = In.decode_int(packet) slot = In.decode_short(packet) item_id = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate item is recipe (item_id / 10000 == 251) # TODO: Apply recipe effect (learn skill) # TODO: Remove recipe item Logger.debug("Use recipe: item #{item_id} at slot #{slot}, tick #{tick}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to use recipe: #{inspect(reason)}") end end # ============================================================================ # Extractor # ============================================================================ @doc """ Handles extractor creation (CP_MAKE_EXTRACTOR / 0x114). Extractors allow other players to use your profession skills. Reference: ItemMakerHandler.MakeExtractor() """ def handle_make_extractor(packet, client_pid) do item_id = In.decode_int(packet) fee = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # Handle removing extractor (negative item_id) if item_id < 0 do # TODO: Remove extractor Logger.debug("Remove extractor: character #{character_id}") else # TODO: Validate item is extractor (item_id / 10000 == 304) # TODO: Validate fee > 0 # TODO: Validate in town # TODO: Create extractor on map Logger.debug("Make extractor: item #{item_id}, fee #{fee}, character #{character_id}") end send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to make extractor: #{inspect(reason)}") end end # ============================================================================ # Bags # ============================================================================ @doc """ Handles bag usage (CP_USE_BAG / 0x68). Herb bags and mining bags extend inventory. Reference: ItemMakerHandler.UseBag() """ def handle_use_bag(packet, client_pid) do tick = In.decode_int(packet) slot = In.decode_short(packet) item_id = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate item is bag (item_id / 10000 == 433) # TODO: Add to extended slots if first time # TODO: Open bag UI Logger.debug("Use bag: item #{item_id} at slot #{slot}, tick #{tick}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to use bag: #{inspect(reason)}") end end # ============================================================================ # Harvesting # ============================================================================ @doc """ Handles start harvest (CP_START_HARVEST / 0x12E). Reference: ItemMakerHandler.StartHarvest() """ def handle_start_harvest(packet, client_pid) do reactor_oid = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Get reactor from map # TODO: Validate reactor is valid for harvesting # TODO: Check harvesting tool # TODO: Check fatigue # TODO: Check harvest cooldown # TODO: Send harvest OK message Logger.debug("Start harvest: reactor #{reactor_oid}, character #{character_id}") :ok {:error, reason} -> Logger.warn("Failed to start harvest: #{inspect(reason)}") end end @doc """ Handles stop harvest (CP_STOP_HARVEST / 0x12F). Reference: ItemMakerHandler.StopHarvest() """ def handle_stop_harvest(packet, client_pid) do reactor_oid = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Process harvest completion # TODO: Give items # TODO: Destroy reactor # TODO: Trigger reactor script Logger.debug("Stop harvest: reactor #{reactor_oid}, character #{character_id}") :ok {:error, reason} -> Logger.warn("Failed to stop harvest: #{inspect(reason)}") end end # ============================================================================ # Profession Info # ============================================================================ @doc """ Handles profession info request (CP_PROFESSION_INFO / 0x97). Reference: ItemMakerHandler.ProfessionInfo() """ def handle_profession_info(packet, client_pid) do profession_str = In.decode_string(packet) level1 = In.decode_int(packet) level2 = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # Parse profession string to get skill ID profession_id = String.to_integer(profession_str) # Calculate progress percentage # progress = max(0, 100 - ((level1 + 1) - profession_level) * 20) # TODO: Send profession info packet # packet = Packets.profession_info(profession_str, level1, level2, progress) # send(client_pid, {:send_packet, packet}) Logger.debug("Profession info: #{profession_id}, levels #{level1}/#{level2}, character #{character_id}") :ok {:error, reason} -> Logger.warn("Failed to get profession info: #{inspect(reason)}") end end # ============================================================================ # Crafting Animations # ============================================================================ @doc """ Handles crafting effect (CP_CRAFT_EFFECT / 0xC9). Shows crafting animation to player and others. Reference: ItemMakerHandler.CraftEffect() """ def handle_craft_effect(packet, client_pid) do effect = In.decode_string(packet) time = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # Validate map (Ardentmill or has extractor) valid_map = char_state.map == 910001000 #|| has_extractor_nearby? if valid_map do profession = Map.get(@crafting_effects, effect) if profession do # Clamp time to 3-6 seconds time = max(3000, min(6000, time)) is_extract = String.ends_with?(effect, "Extract") # TODO: Broadcast crafting effect # packet = Packets.show_own_crafting_effect(effect, time, is_extract) # send(client_pid, {:send_packet, packet}) # TODO: Broadcast to others # packet = Packets.show_crafting_effect(character_id, effect, time, is_extract) # Map.broadcast_packet(char_state.map, packet, exclude: character_id) end end Logger.debug("Craft effect: #{effect}, time #{time}, character #{character_id}") :ok {:error, reason} -> Logger.warn("Failed to handle craft effect: #{inspect(reason)}") end end @doc """ Handles craft make animation (CP_CRAFT_MAKE / 0xCA). Broadcasts crafting animation to map. Reference: ItemMakerHandler.CraftMake() """ def handle_craft_make(packet, client_pid) do something = In.decode_int(packet) time = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # Clamp time time = max(3000, min(6000, time)) # TODO: Broadcast craft make animation # packet = Packets.craft_make(character_id, something, time) # Map.broadcast_packet(char_state.map, packet) Logger.debug("Craft make: #{something}, time #{time}, character #{character_id}") :ok {:error, reason} -> Logger.warn("Failed to handle craft make: #{inspect(reason)}") end end @doc """ Handles craft completion (CP_CRAFT_DONE / 0xC8). Processes crafting results. Reference: ItemMakerHandler.CraftComplete() """ def handle_craft_complete(packet, client_pid) do craft_id = In.decode_int(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Get crafting entry from SkillFactory # ce = SkillFactory.get_craft(craft_id) # TODO: Check profession level # TODO: Check fatigue # TODO: Process disassembly, fusing, or normal crafting # TODO: Calculate success/failure # TODO: Give items # TODO: Add profession EXP # TODO: Add fatigue Logger.debug("Craft complete: #{craft_id}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to complete craft: #{inspect(reason)}") end end # ============================================================================ # Item Pot (Imps) # ============================================================================ @doc """ Handles use item pot (CP_USE_POT / 0x98). Summons an item pot (imp) pet. Reference: ItemMakerHandler.UsePot() """ def handle_use_pot(packet, client_pid) do item_id = In.decode_int(packet) slot = In.decode_short(packet) case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate item is pot item (item_id / 10000 == 244) # TODO: Check for empty imp slot # TODO: Create imp # TODO: Remove item Logger.debug("Use pot: item #{item_id} at slot #{slot}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to use pot: #{inspect(reason)}") end end @doc """ Handles clear item pot (CP_CLEAR_POT / 0x99). Removes an item pot. Reference: ItemMakerHandler.ClearPot() """ def handle_clear_pot(packet, client_pid) do index = In.decode_int(packet) - 1 case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate index # TODO: Remove imp Logger.debug("Clear pot: index #{index}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to clear pot: #{inspect(reason)}") end end @doc """ Handles feed item pot (CP_FEED_POT / 0x9A). Feeds item to imp to level it up. Reference: ItemMakerHandler.FeedPot() """ def handle_feed_pot(packet, client_pid) do item_id = In.decode_int(packet) slot = In.decode_int(packet) index = In.decode_int(packet) - 1 case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate imp exists # TODO: Validate item level range # TODO: Add fullness/closeness # TODO: Level up if full # TODO: Remove item Logger.debug("Feed pot: item #{item_id} at #{slot}, index #{index}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to feed pot: #{inspect(reason)}") end end @doc """ Handles cure item pot (CP_CURE_POT / 0x9B). Cures a sick imp. Reference: ItemMakerHandler.CurePot() """ def handle_cure_pot(packet, client_pid) do item_id = In.decode_int(packet) slot = In.decode_int(packet) index = In.decode_int(packet) - 1 case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate imp is sick # TODO: Validate cure item (item_id / 10000 == 434) # TODO: Cure imp # TODO: Remove item Logger.debug("Cure pot: item #{item_id} at #{slot}, index #{index}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to cure pot: #{inspect(reason)}") end end @doc """ Handles reward from item pot (CP_REWARD_POT / 0x9C). Claims reward from fully grown imp. Reference: ItemMakerHandler.RewardPot() """ def handle_reward_pot(packet, client_pid) do index = In.decode_int(packet) - 1 case Character.get_state_by_client(client_pid) do {:ok, character_id, char_state} -> # TODO: Validate imp is max level # TODO: Calculate reward based on imp type and closeness # TODO: Give reward item # TODO: Remove imp Logger.debug("Reward pot: index #{index}, character #{character_id}") send(client_pid, {:send_packet, Packets.enable_actions()}) :ok {:error, reason} -> Logger.warn("Failed to reward pot: #{inspect(reason)}") end end # ============================================================================ # Helper Functions # ============================================================================ defp is_gem?(item_id) do # Gems are in specific ID ranges # TODO: Implement proper check from GameConstants item_id >= 4000000 and item_id < 4001000 end defp is_other_gem?(item_id) do # Other items that use gem crafting # TODO: Implement proper check from GameConstants false end end