357 lines
11 KiB
Elixir
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
|