defmodule Odinsea.Channel.Handler.Player do @moduledoc """ Handles player action packets (movement, attacks, map changes). Ported from src/handling/channel/handler/PlayerHandler.java """ require Logger alias Odinsea.Net.Packet.{In, Out} alias Odinsea.Net.Opcodes alias Odinsea.Channel.Packets alias Odinsea.Game.{Character, Movement, Map, AttackInfo, DamageCalc} @doc """ Handles player movement (CP_MOVE_PLAYER). Ported from PlayerHandler.MovePlayer() """ def handle_move_player(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid), {:ok, map_pid} <- get_map_pid(character.map_id, client_state.channel_id) do # Decode movement header {_dr0, packet} = In.decode_int(packet) {_dr1, packet} = In.decode_int(packet) # TODO: Check field key {_dr2, packet} = In.decode_int(packet) {_dr3, packet} = In.decode_int(packet) # Skip 20 bytes {_, packet} = In.skip(packet, 20) # Store original position original_pos = character.position # Parse movement using the full movement system case Movement.parse_player_movement(packet, original_pos) do {:ok, movements, final_pos} -> # Update character position Character.update_position(character_pid, final_pos) # Serialize movements for broadcast movement_data = Movement.serialize_movements(movements) # Broadcast movement to other players move_packet = Out.new(Opcodes.lp_move_player()) |> Out.encode_int(character.id) |> Out.encode_short(original_pos.x) |> Out.encode_short(original_pos.y) |> Out.encode_int(0) # Unknown int |> Out.encode_bytes(movement_data) |> Out.to_data() Map.broadcast_except( character.map_id, client_state.channel_id, character.id, move_packet ) Logger.debug( "Player #{character.name} moved from (#{original_pos.x}, #{original_pos.y}) to (#{final_pos.x}, #{final_pos.y}) with #{length(movements)} movements" ) {:ok, client_state} {:error, reason} -> Logger.warning("Movement parsing failed: #{inspect(reason)}") {:ok, client_state} end else {:error, reason} -> Logger.warning("Move player failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles map change via portal (CP_CHANGE_MAP). Ported from PlayerHandler.ChangeMap() """ def handle_change_map(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do # TODO: Check field key {target_id, packet} = In.decode_int(packet) # Skip GMS-specific field {_, packet} = In.decode_int(packet) {portal_name, packet} = In.decode_string(packet) Logger.info( "Character #{character.name} changing map: target=#{target_id}, portal=#{portal_name}" ) # Handle different map change scenarios cond do # Death respawn target_id == -1 and not character.alive? -> # Respawn at return map # TODO: Implement death respawn logic Logger.info("Player #{character.name} respawning") {:ok, client_state} # GM warp to specific map target_id != -1 and character.gm? -> # TODO: Implement GM warp Logger.info("GM #{character.name} warping to map #{target_id}") {:ok, client_state} # Portal-based map change true -> # TODO: Load portal data and handle map transition # For now, just log the request Logger.info( "Portal map change: #{character.name} using portal '#{portal_name}'" ) {:ok, client_state} end else {:error, reason} -> Logger.warning("Change map failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles keymap changes (CP_CHANGE_KEYMAP). Ported from PlayerHandler.ChangeKeymap() """ def handle_change_keymap(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, _character} <- Character.get_state(character_pid) do # Skip mode {_, packet} = In.skip(packet, 4) {num_changes, packet} = In.decode_int(packet) # Parse keybinding changes keybindings = parse_keybindings(packet, num_changes, []) # TODO: Store keybindings in character state / database Logger.debug("Keybindings updated: #{num_changes} changes") {:ok, client_state} else {:error, reason} -> Logger.warning("Change keymap failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles skill macro changes (CP_CHANGE_SKILL_MACRO). Ported from PlayerHandler.ChangeSkillMacro() """ def handle_change_skill_macro(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, _character} <- Character.get_state(character_pid) do {num_macros, packet} = In.decode_byte(packet) # Parse macros macros = parse_macros(packet, num_macros, []) # TODO: Store macros in character state / database Logger.debug("Skill macros updated: #{num_macros} macros") {:ok, client_state} else {:error, reason} -> Logger.warning("Change skill macro failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles close-range attack (CP_CLOSE_RANGE_ATTACK). Ported from PlayerHandler.closeRangeAttack() and DamageParse.parseDmgM() """ def handle_close_range_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid), {:ok, map_pid} <- get_map_pid(character.map_id, client_state.channel_id) do # Parse attack packet case AttackInfo.parse_melee_attack(packet) do {:ok, attack_info} -> Logger.debug( "Close range attack from #{character.name}: skill=#{attack_info.skill}, targets=#{attack_info.targets}, hits=#{attack_info.hits}" ) # Apply attack via DamageCalc case DamageCalc.apply_attack( attack_info, character_pid, map_pid, client_state.channel_id ) do {:ok, total_damage} -> Logger.debug("Attack dealt #{total_damage} total damage") {:ok, client_state} {:error, reason} -> Logger.warning("Attack failed: #{inspect(reason)}") {:ok, client_state} end {:error, reason} -> Logger.warning("Failed to parse melee attack: #{inspect(reason)}") {:ok, client_state} end else {:error, reason} -> Logger.warning("Close range attack failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles ranged attack (CP_RANGED_ATTACK). Ported from PlayerHandler.rangedAttack() and DamageParse.parseDmgR() """ def handle_ranged_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid), {:ok, map_pid} <- get_map_pid(character.map_id, client_state.channel_id) do # Parse attack packet case AttackInfo.parse_ranged_attack(packet) do {:ok, attack_info} -> Logger.debug( "Ranged attack from #{character.name}: skill=#{attack_info.skill}, targets=#{attack_info.targets}, hits=#{attack_info.hits}" ) # Apply attack via DamageCalc case DamageCalc.apply_attack( attack_info, character_pid, map_pid, client_state.channel_id ) do {:ok, total_damage} -> Logger.debug("Attack dealt #{total_damage} total damage") {:ok, client_state} {:error, reason} -> Logger.warning("Attack failed: #{inspect(reason)}") {:ok, client_state} end {:error, reason} -> Logger.warning("Failed to parse ranged attack: #{inspect(reason)}") {:ok, client_state} end else {:error, reason} -> Logger.warning("Ranged attack failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles magic attack (CP_MAGIC_ATTACK). Ported from PlayerHandler.MagicDamage() and DamageParse.parseDmgMa() """ def handle_magic_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid), {:ok, map_pid} <- get_map_pid(character.map_id, client_state.channel_id) do # Parse attack packet case AttackInfo.parse_magic_attack(packet) do {:ok, attack_info} -> Logger.debug( "Magic attack from #{character.name}: skill=#{attack_info.skill}, targets=#{attack_info.targets}, hits=#{attack_info.hits}" ) # Apply attack via DamageCalc case DamageCalc.apply_attack( attack_info, character_pid, map_pid, client_state.channel_id ) do {:ok, total_damage} -> Logger.debug("Attack dealt #{total_damage} total damage") {:ok, client_state} {:error, reason} -> Logger.warning("Attack failed: #{inspect(reason)}") {:ok, client_state} end {:error, reason} -> Logger.warning("Failed to parse magic attack: #{inspect(reason)}") {:ok, client_state} end else {:error, reason} -> Logger.warning("Magic attack failed: #{inspect(reason)}") {:ok, client_state} end end @doc """ Handles taking damage (CP_TAKE_DAMAGE). Ported from PlayerHandler.TakeDamage() - STUB for now """ def handle_take_damage(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do # Decode damage packet {_tick, packet} = In.decode_int(packet) {damage_type, packet} = In.decode_byte(packet) {element, packet} = In.decode_byte(packet) {damage, packet} = In.decode_int(packet) Logger.debug( "Character #{character.name} took #{damage} damage (type=#{damage_type}, element=#{element})" ) # TODO: Apply damage to character # TODO: Check for death # TODO: Broadcast damage packet {:ok, client_state} else {:error, reason} -> Logger.warning("Take damage failed: #{inspect(reason)}") {:ok, client_state} end end # ============================================================================ # Private Helper Functions # ============================================================================ defp get_character(client_state) do case client_state.character_id do nil -> {:error, :no_character} character_id -> case Registry.lookup(Odinsea.CharacterRegistry, character_id) do [{pid, _}] -> {:ok, pid} [] -> {:error, :character_not_found} end end end defp get_map_pid(map_id, channel_id) do case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do [{pid, _}] -> {:ok, pid} [] -> # Map not loaded yet - load it case DynamicSupervisor.start_child( Odinsea.MapSupervisor, {Map, {map_id, channel_id}} ) do {:ok, pid} -> {:ok, pid} {:error, {:already_started, pid}} -> {:ok, pid} error -> error end end end defp parse_keybindings(packet, 0, acc), do: Enum.reverse(acc) defp parse_keybindings(packet, count, acc) do {key, packet} = In.decode_int(packet) {key_type, packet} = In.decode_byte(packet) {action, packet} = In.decode_int(packet) binding = %{key: key, type: key_type, action: action} parse_keybindings(packet, count - 1, [binding | acc]) end defp parse_macros(packet, 0, acc), do: Enum.reverse(acc) defp parse_macros(packet, count, acc) do {name, packet} = In.decode_string(packet) {shout, packet} = In.decode_byte(packet) {skill1, packet} = In.decode_int(packet) {skill2, packet} = In.decode_int(packet) {skill3, packet} = In.decode_int(packet) macro = %{ name: name, shout: shout, skill1: skill1, skill2: skill2, skill3: skill3 } parse_macros(packet, count - 1, [macro | acc]) end end