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} @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 case Movement.parse_movement(packet) do {:ok, movement_data, final_pos} -> # Update character position Character.update_position(character_pid, final_pos) # Broadcast movement to other players move_packet = Out.new(Opcodes.lp_move_player()) |> Out.encode_int(character.id) |> 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 to (#{final_pos.x}, #{final_pos.y})" ) {: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() - STUB for now """ def handle_close_range_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do Logger.debug("Close range attack from #{character.name} (stub)") # TODO: Implement attack logic # - Parse attack info # - Validate attack # - Calculate damage # - Apply damage to mobs # - Broadcast attack packet {:ok, client_state} 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() - STUB for now """ def handle_ranged_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do Logger.debug("Ranged attack from #{character.name} (stub)") # TODO: Implement ranged attack logic {:ok, client_state} 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() - STUB for now """ def handle_magic_attack(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do Logger.debug("Magic attack from #{character.name} (stub)") # TODO: Implement magic attack logic {:ok, client_state} 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