Files
odinsea-elixir/lib/odinsea/channel/handler/item_maker.ex
2026-02-14 23:12:33 -07:00

642 lines
20 KiB
Elixir

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