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

357 lines
11 KiB
Elixir

defmodule Odinsea.Channel.Handler.Mob do
@moduledoc """
Handles all mob (monster) related packets from the client.
Ported from: src/handling/channel/handler/MobHandler.java
## Main Handlers
- handle_mob_move/2 - Monster movement from controller
- handle_auto_aggro/2 - Monster aggro request
- handle_mob_skill_delay_end/2 - Monster skill execution
- handle_mob_bomb/2 - Monster self-destruct
- handle_mob_hit_by_mob/2 - Mob to mob damage
"""
require Logger
use Bitwise
alias Odinsea.Net.Packet.In
alias Odinsea.Game.{Character, Map, Movement}
alias Odinsea.Game.Movement.Path
alias Odinsea.Channel.Packets
# ============================================================================
# Packet Handlers
# ============================================================================
@doc """
Handles monster movement from the controlling client (CP_MOVE_LIFE / 0xF3).
Flow:
1. Client sends mob movement when they control the mob
2. Server validates the movement
3. Server broadcasts movement to other players
Reference: MobHandler.onMobMove()
"""
def handle_mob_move(packet, client_pid) do
# Decode packet
mob_id = In.decode_int(packet)
mob_ctrl_sn = In.decode_short(packet)
mob_ctrl_state = In.decode_byte(packet)
next_attack_possible = (mob_ctrl_state &&& 0x0F) != 0
action = In.decode_byte(packet)
data = In.decode_int(packet)
# Multi-target for ball
multi_target_count = In.decode_int(packet)
packet = Enum.reduce(1..multi_target_count, packet, fn _, acc_packet ->
acc_packet
|> In.decode_int() # x
|> In.decode_int() # y
end)
# Rand time for area attack
rand_time_count = In.decode_int(packet)
packet = Enum.reduce(1..rand_time_count, packet, fn _, acc_packet ->
In.decode_int(acc_packet) # rand time
end)
# Movement validation fields
_is_cheat_mob_move_rand = In.decode_byte(packet)
_hacked_code = In.decode_int(packet)
_target_x = In.decode_int(packet)
_target_y = In.decode_int(packet)
_hacked_code_crc = In.decode_int(packet)
# Parse MovePath (newer mob movement system)
move_path = Path.decode(packet, false)
# Parse additional passive data
_b_chasing = In.decode_byte(packet)
_has_target = In.decode_byte(packet)
_target_b_chasing = In.decode_byte(packet)
_target_b_chasing_hack = In.decode_byte(packet)
_chase_duration = In.decode_int(packet)
# Get character state
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# Update monster position if path has elements
final_pos = Path.get_final_position(move_path)
final_action = Path.get_final_action(move_path)
final_foothold = Path.get_final_foothold(move_path)
# TODO: Validate monster controller
# TODO: Update monster in map
# TODO: Broadcast movement to other players
Logger.debug(
"Mob move: OID #{mob_id}, action #{action}, " <>
"pos (#{final_pos.x}, #{final_pos.y}), " <>
"elements #{length(move_path.elements)}, character #{character_id}"
)
# Send control ack back to client
ack_packet = Packets.mob_ctrl_ack(mob_id, mob_ctrl_sn, next_attack_possible, 100, 0, 0)
send(client_pid, {:send_packet, ack_packet})
# Broadcast movement to other players if path has elements
if length(move_path.elements) > 0 do
broadcast_mob_move(
char_state.map,
char_state.channel_id,
mob_id,
next_attack_possible,
action,
move_path,
character_id
)
end
:ok
{:error, reason} ->
Logger.warn("Failed to handle mob move: #{inspect(reason)}")
end
end
@doc """
Handles monster auto-aggro (CP_AUTO_AGGRO / 0xFC).
When a monster detects a player, the client sends this packet to request control.
Reference: MobHandler.AutoAggro()
"""
def handle_auto_aggro(packet, client_pid) do
monster_oid = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
map_id = char_state.map
channel_id = char_state.channel_id
# TODO: Implement controller assignment
# TODO: Check distance between player and monster
# TODO: Assign monster control to this player
Logger.debug("Auto aggro: Monster OID #{monster_oid}, character #{character_id}")
# For now, just acknowledge
:ok
{:error, reason} ->
Logger.warn("Failed to handle auto aggro: #{inspect(reason)}")
end
end
@doc """
Handles monster skill delay end (CP_MOB_SKILL_DELAY_END / 0xFE).
After a monster skill animation delay, the client sends this to execute the skill effect.
Reference: MobHandler.onMobSkillDelayEnd()
"""
def handle_mob_skill_delay_end(packet, client_pid) do
monster_oid = In.decode_int(packet)
skill_id = In.decode_int(packet)
skill_lv = In.decode_int(packet)
# _option = In.decode_int(packet) # Sometimes present
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Validate monster has this skill
# TODO: Execute mob skill effect (stun, poison, etc.)
# TODO: Apply skill to players in range
Logger.debug("Mob skill delay end: OID #{monster_oid}, skill #{skill_id} lv #{skill_lv}, character #{character_id}")
{:error, reason} ->
Logger.warn("Failed to handle mob skill delay end: #{inspect(reason)}")
end
end
@doc """
Handles monster bomb/self-destruct (CP_MOB_BOMB / 0xFF).
Some monsters explode when their timer runs out or when triggered.
Reference: MobHandler.MobBomb()
"""
def handle_mob_bomb(packet, client_pid) do
monster_oid = In.decode_int(packet)
_unknown = In.decode_short(packet) # 9E 07 or similar
_damage = In.decode_int(packet) # -204 or similar
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
map_id = char_state.map
channel_id = char_state.channel_id
# TODO: Check if monster has TimeBomb buff
# TODO: Execute bomb explosion
# TODO: Damage players in range
# TODO: Kill monster
Logger.debug("Mob bomb: OID #{monster_oid}, character #{character_id}")
{:error, reason} ->
Logger.warn("Failed to handle mob bomb: #{inspect(reason)}")
end
end
@doc """
Handles mob-to-mob damage (when monsters attack each other).
Used for friendly mobs like Shammos escort quests.
Reference: MobHandler.OnMobHitByMob(), MobHandler.OnMobAttackMob()
"""
def handle_mob_hit_by_mob(packet, client_pid) do
mob_from_oid = In.decode_int(packet)
_player_id = In.decode_int(packet)
mob_to_oid = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
map_id = char_state.map
channel_id = char_state.channel_id
# TODO: Validate both monsters exist
# TODO: Check if target monster is friendly
# TODO: Calculate and apply damage
# TODO: Check for special escort quest logic (Shammos)
Logger.debug("Mob hit by mob: From OID #{mob_from_oid}, to OID #{mob_to_oid}, character #{character_id}")
{:error, reason} ->
Logger.warn("Failed to handle mob hit by mob: #{inspect(reason)}")
end
end
@doc """
Handles mob-to-mob attack damage packet (CP_MOB_ATTACK_MOB).
Similar to handle_mob_hit_by_mob but with more damage information.
Reference: MobHandler.OnMobAttackMob()
"""
def handle_mob_attack_mob(packet, client_pid) do
mob_from_oid = In.decode_int(packet)
_player_id = In.decode_int(packet)
mob_to_oid = In.decode_int(packet)
_skill_or_bump = In.decode_byte(packet) # -1 = bump, otherwise skill ID
damage = In.decode_int(packet)
# Damage cap check
if damage > 30_000 do
Logger.warn("Suspicious mob-to-mob damage: #{damage}")
:ok
else
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
map_id = char_state.map
channel_id = char_state.channel_id
# TODO: Validate both monsters exist
# TODO: Check if target monster is friendly
# TODO: Apply damage to target monster
# TODO: Broadcast damage packet
# TODO: Check for Shammos escort quest logic
Logger.debug("Mob attack mob: From OID #{mob_from_oid}, to OID #{mob_to_oid}, damage #{damage}, character #{character_id}")
{:error, reason} ->
Logger.warn("Failed to handle mob attack mob: #{inspect(reason)}")
end
end
end
@doc """
Handles monster escort collision (CP_MOB_ESCORT_COLLISION).
Used for escort quests where monsters follow a path with nodes.
Reference: MobHandler.OnMobEscrotCollision()
"""
def handle_mob_escort_collision(packet, client_pid) do
mob_oid = In.decode_int(packet)
new_node = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate monster is an escort type
# TODO: Update monster's current node
# TODO: Check if node triggers dialog
# TODO: Check if node is last node (quest complete)
Logger.debug("Mob escort collision: OID #{mob_oid}, node #{new_node}, character #{character_id}")
{:error, reason} ->
Logger.warn("Failed to handle mob escort collision: #{inspect(reason)}")
end
end
@doc """
Handles monster escort info request (CP_MOB_REQUEST_ESCORT_INFO).
Client requests path information for an escort monster.
Reference: MobHandler.OnMobRequestEscortInfo()
"""
def handle_mob_request_escort_info(packet, client_pid) do
mob_oid = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, _character_id, _char_state} ->
# TODO: Get monster from map
# TODO: Get map node properties
# TODO: Send node properties packet to client
Logger.debug("Mob escort info request: OID #{mob_oid}")
{:error, reason} ->
Logger.warn("Failed to handle mob escort info request: #{inspect(reason)}")
end
end
# ============================================================================
# Helper Functions
# ============================================================================
@doc false
def get_character_state(client_pid) do
Character.get_state_by_client(client_pid)
end
# Broadcast mob movement to other players in the map
defp broadcast_mob_move(
map_id,
channel_id,
mob_id,
next_attack_possible,
action,
move_path,
controller_id
) do
# Encode movement data
move_path_data = Path.encode(move_path, false)
# Build movement packet
# LP_MobMove packet structure:
# - mob_id (int)
# - byte (0)
# - byte (0)
# - next_attack_possible (bool)
# - action (byte)
# - skill_id (int)
# - multi_target (int, 0)
# - rand_time (int, 0)
# - move_path_data
# TODO: Build and broadcast actual packet via Map.broadcast_except
# For now just log
Logger.debug("Broadcasting mob #{mob_id} move to map #{map_id} (controller: #{controller_id})")
end
end