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