defmodule Odinsea.Game.AttackInfo do use Bitwise @moduledoc """ Attack information struct and parser functions. Ported from src/handling/channel/handler/AttackInfo.java and DamageParse.java """ alias Odinsea.Net.Packet.In require Logger @type attack_type :: :melee | :ranged | :magic | :melee_with_mirror | :ranged_with_shadowpartner @type damage_entry :: %{ mob_oid: integer(), damages: list({integer(), boolean()}) } @type t :: %__MODULE__{ skill: integer(), charge: integer(), last_attack_tick: integer(), all_damage: list(damage_entry()), position: %{x: integer(), y: integer()}, display: integer(), hits: integer(), targets: integer(), tbyte: integer(), speed: integer(), csstar: integer(), aoe: integer(), slot: integer(), unk: integer(), delay: integer(), real: boolean(), attack_type: attack_type() } defstruct [ :skill, :charge, :last_attack_tick, :all_damage, :position, :display, :hits, :targets, :tbyte, :speed, :csstar, :aoe, :slot, :unk, :delay, :real, :attack_type ] @doc """ Parse melee/close-range attack packet (CP_CLOSE_RANGE_ATTACK). Ported from DamageParse.parseDmgM() """ def parse_melee_attack(packet, opts \\ []) do energy = Keyword.get(opts, :energy, false) # Decode attack header {tbyte, packet} = In.decode_byte(packet) targets = (tbyte >>> 4) &&& 0xF hits = tbyte &&& 0xF {skill, packet} = In.decode_int(packet) # Skip GMS-specific fields (9 bytes in GMS) {_, packet} = In.skip(packet, 9) # Handle charge skills {charge, packet} = case skill do 5_101_004 -> In.decode_int(packet) # Corkscrew 15_101_003 -> In.decode_int(packet) # Cygnus corkscrew 5_201_002 -> In.decode_int(packet) # Gernard 14_111_006 -> In.decode_int(packet) # Poison bomb 4_341_002 -> In.decode_int(packet) 4_341_003 -> In.decode_int(packet) 5_301_001 -> In.decode_int(packet) 5_300_007 -> In.decode_int(packet) _ -> {0, packet} end {unk, packet} = In.decode_byte(packet) {display, packet} = In.decode_ushort(packet) # Skip 4 bytes (big bang) + 1 byte (weapon class) {_, packet} = In.skip(packet, 5) {speed, packet} = In.decode_byte(packet) {last_attack_tick, packet} = In.decode_int(packet) # Skip 4 bytes (padding) {_, packet} = In.skip(packet, 4) # Meso Explosion special handling if skill == 4_211_006 do parse_meso_explosion(packet, %__MODULE__{ skill: skill, charge: charge, last_attack_tick: last_attack_tick, display: display, hits: hits, targets: targets, tbyte: tbyte, speed: speed, unk: unk, real: true, attack_type: :melee }) else # Parse damage for each target {all_damage, packet} = parse_damage_targets(packet, targets, hits, []) {position, _packet} = In.decode_point(packet) {:ok, %__MODULE__{ skill: skill, charge: charge, last_attack_tick: last_attack_tick, all_damage: all_damage, position: position, display: display, hits: hits, targets: targets, tbyte: tbyte, speed: speed, unk: unk, real: true, attack_type: :melee }} end end @doc """ Parse ranged attack packet (CP_RANGED_ATTACK). Ported from DamageParse.parseDmgR() """ def parse_ranged_attack(packet) do # Decode attack header {tbyte, packet} = In.decode_byte(packet) targets = (tbyte >>> 4) &&& 0xF hits = tbyte &&& 0xF {skill, packet} = In.decode_int(packet) # Skip GMS-specific fields (10 bytes in GMS) {_, packet} = In.skip(packet, 10) # Handle special skills with extra 4 bytes {_, packet} = case skill do 3_121_004 -> In.skip(packet, 4) # Hurricane 3_221_001 -> In.skip(packet, 4) # Pierce 5_221_004 -> In.skip(packet, 4) # Rapidfire 13_111_002 -> In.skip(packet, 4) # Cygnus Hurricane 33_121_009 -> In.skip(packet, 4) 35_001_001 -> In.skip(packet, 4) 35_101_009 -> In.skip(packet, 4) 23_121_000 -> In.skip(packet, 4) 5_311_002 -> In.skip(packet, 4) _ -> {nil, packet} end {unk, packet} = In.decode_byte(packet) {display, packet} = In.decode_ushort(packet) # Skip 4 bytes (big bang) + 1 byte (weapon class) {_, packet} = In.skip(packet, 5) {speed, packet} = In.decode_byte(packet) {last_attack_tick, packet} = In.decode_int(packet) # Skip 4 bytes (padding) {_, packet} = In.skip(packet, 4) {slot, packet} = In.decode_short(packet) {csstar, packet} = In.decode_short(packet) {aoe, packet} = In.decode_byte(packet) # Parse damage for each target {all_damage, packet} = parse_damage_targets(packet, targets, hits, []) # Skip 4 bytes before position {_, packet} = In.skip(packet, 4) {position, _packet} = In.decode_point(packet) {:ok, %__MODULE__{ skill: skill, charge: -1, last_attack_tick: last_attack_tick, all_damage: all_damage, position: position, display: display, hits: hits, targets: targets, tbyte: tbyte, speed: speed, csstar: csstar, aoe: aoe, slot: slot, unk: unk, real: true, attack_type: :ranged }} end @doc """ Parse magic attack packet (CP_MAGIC_ATTACK). Ported from DamageParse.parseDmgMa() """ def parse_magic_attack(packet) do # Decode attack header {tbyte, packet} = In.decode_byte(packet) targets = (tbyte >>> 4) &&& 0xF hits = tbyte &&& 0xF {skill, packet} = In.decode_int(packet) # Return error if invalid skill if skill >= 91_000_000 do {:error, :invalid_skill} else # Skip GMS-specific fields (9 bytes in GMS) {_, packet} = In.skip(packet, 9) # Handle charge skills {charge, packet} = if is_magic_charge_skill?(skill) do In.decode_int(packet) else {-1, packet} end {unk, packet} = In.decode_byte(packet) {display, packet} = In.decode_ushort(packet) # Skip 4 bytes (big bang) + 1 byte (weapon class) {_, packet} = In.skip(packet, 5) {speed, packet} = In.decode_byte(packet) {last_attack_tick, packet} = In.decode_int(packet) # Skip 4 bytes (padding) {_, packet} = In.skip(packet, 4) # Parse damage for each target {all_damage, packet} = parse_damage_targets(packet, targets, hits, []) {position, _packet} = In.decode_point(packet) {:ok, %__MODULE__{ skill: skill, charge: charge, last_attack_tick: last_attack_tick, all_damage: all_damage, position: position, display: display, hits: hits, targets: targets, tbyte: tbyte, speed: speed, unk: unk, real: true, attack_type: :magic }} end end # ============================================================================ # Private Helper Functions # ============================================================================ defp parse_damage_targets(_packet, 0, _hits, acc), do: {Enum.reverse(acc), <<>>} defp parse_damage_targets(packet, targets_remaining, hits, acc) do {mob_oid, packet} = In.decode_int(packet) # Skip 12 bytes: [1] Always 6?, [3] unk, [4] Pos1, [4] Pos2 {_, packet} = In.skip(packet, 12) {delay, packet} = In.decode_short(packet) # Parse damage values for this target {damages, packet} = parse_damage_hits(packet, hits, []) # Skip 4 bytes: CRC of monster [Wz Editing] {_, packet} = In.skip(packet, 4) damage_entry = %{ mob_oid: mob_oid, damages: damages, delay: delay } parse_damage_targets(packet, targets_remaining - 1, hits, [damage_entry | acc]) end defp parse_damage_hits(_packet, 0, acc), do: {Enum.reverse(acc), <<>>} defp parse_damage_hits(packet, hits_remaining, acc) do {damage, packet} = In.decode_int(packet) # Second boolean is for critical hit (not used in v342) parse_damage_hits(packet, hits_remaining - 1, [{damage, false} | acc]) end defp parse_meso_explosion(packet, attack_info) do # TODO: Implement meso explosion parsing # For now, return empty damage list Logger.warning("Meso explosion not yet implemented") {:ok, %{attack_info | all_damage: [], position: %{x: 0, y: 0}}} end defp is_magic_charge_skill?(skill_id) do skill_id in [ # Big Bang skills 2_121_001, 2_221_001, 2_321_001, # Elemental Charge skills 12_111_004 ] end end