336 lines
8.8 KiB
Elixir
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
|