642 lines
20 KiB
Elixir
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
|