kimi gone wild
This commit is contained in:
356
lib/odinsea/channel/handler/mob.ex
Normal file
356
lib/odinsea/channel/handler/mob.ex
Normal file
@@ -0,0 +1,356 @@
|
||||
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
|
||||
Reference in New Issue
Block a user