Files
odinsea-elixir/lib/odinsea/game/attack_info.ex
2026-02-14 23:12:33 -07:00

336 lines
8.8 KiB
Elixir

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