kimi gone wild
This commit is contained in:
335
lib/odinsea/game/attack_info.ex
Normal file
335
lib/odinsea/game/attack_info.ex
Normal file
@@ -0,0 +1,335 @@
|
||||
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
|
||||
Reference in New Issue
Block a user