kimi gone wild
This commit is contained in:
@@ -6,6 +6,7 @@ defmodule Odinsea.Channel.Packets do
|
||||
|
||||
alias Odinsea.Net.Packet.Out
|
||||
alias Odinsea.Net.Opcodes
|
||||
alias Odinsea.Game.Reactor
|
||||
|
||||
@doc """
|
||||
Sends character information on login.
|
||||
@@ -298,4 +299,915 @@ defmodule Odinsea.Channel.Packets do
|
||||
|> Out.encode_string(message)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Monster Packets
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Spawns a monster on the map (LP_MobEnterField).
|
||||
|
||||
## Parameters
|
||||
- monster: Monster.t() struct
|
||||
- spawn_type: spawn animation type (-1 = normal, -2 = regen, -3 = revive, etc.)
|
||||
- link: linked mob OID (for multi-mobs)
|
||||
|
||||
Reference: MobPacket.spawnMonster()
|
||||
"""
|
||||
def spawn_monster(monster, spawn_type \\ -1, link \\ 0) do
|
||||
Out.new(Opcodes.lp_spawn_monster())
|
||||
|> Out.encode_int(monster.oid)
|
||||
|> Out.encode_byte(1) # 1 = Control normal, 5 = Control none
|
||||
|> Out.encode_int(monster.mob_id)
|
||||
# Temporary stat encoding (buffs/debuffs)
|
||||
|> encode_mob_temporary_stat(monster)
|
||||
# Position
|
||||
|> Out.encode_short(monster.position.x)
|
||||
|> Out.encode_short(monster.position.y)
|
||||
# Move action (bitfield)
|
||||
|> Out.encode_byte(monster.stance)
|
||||
# Foothold SN
|
||||
|> Out.encode_short(monster.fh)
|
||||
# Origin FH
|
||||
|> Out.encode_short(monster.fh)
|
||||
# Spawn type
|
||||
|> Out.encode_byte(spawn_type)
|
||||
# Link OID (for spawn_type -3 or >= 0)
|
||||
|> then(fn packet ->
|
||||
if spawn_type == -3 or spawn_type >= 0 do
|
||||
Out.encode_int(packet, link)
|
||||
else
|
||||
packet
|
||||
end
|
||||
end)
|
||||
# Carnival team
|
||||
|> Out.encode_byte(0)
|
||||
# Aftershock - 8 bytes (0xFF at end for GMS)
|
||||
|> Out.encode_long(0)
|
||||
# GMS specific
|
||||
|> Out.encode_byte(-1)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Assigns monster control to a player (LP_MobChangeController).
|
||||
|
||||
## Parameters
|
||||
- monster: Monster.t() struct
|
||||
- new_spawn: whether this is a new spawn
|
||||
- aggro: aggro mode (2 = aggro, 1 = normal)
|
||||
|
||||
Reference: MobPacket.controlMonster()
|
||||
"""
|
||||
def control_monster(monster, new_spawn \\ false, aggro \\ false) do
|
||||
Out.new(Opcodes.lp_spawn_monster_control())
|
||||
|> Out.encode_byte(if aggro, do: 2, else: 1)
|
||||
|> Out.encode_int(monster.oid)
|
||||
|> Out.encode_byte(1) # 1 = Control normal, 5 = Control none
|
||||
|> Out.encode_int(monster.mob_id)
|
||||
# Temporary stat encoding
|
||||
|> encode_mob_temporary_stat(monster)
|
||||
# Position
|
||||
|> Out.encode_short(monster.position.x)
|
||||
|> Out.encode_short(monster.position.y)
|
||||
# Move action
|
||||
|> Out.encode_byte(monster.stance)
|
||||
# Foothold SN
|
||||
|> Out.encode_short(monster.fh)
|
||||
# Origin FH
|
||||
|> Out.encode_short(monster.fh)
|
||||
# Spawn type (-4 = fake, -2 = new spawn, -1 = normal)
|
||||
|> Out.encode_byte(cond do
|
||||
new_spawn -> -2
|
||||
true -> -1
|
||||
end)
|
||||
# Carnival team
|
||||
|> Out.encode_byte(0)
|
||||
# Big bang - another long
|
||||
|> Out.encode_long(0)
|
||||
# GMS specific
|
||||
|> Out.encode_byte(-1)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops controlling a monster (LP_MobChangeController with byte 0).
|
||||
|
||||
Reference: MobPacket.stopControllingMonster()
|
||||
"""
|
||||
def stop_controlling_monster(oid) do
|
||||
Out.new(Opcodes.lp_spawn_monster_control())
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_int(oid)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Monster movement packet (LP_MobMove).
|
||||
|
||||
## Parameters
|
||||
- oid: monster object ID
|
||||
- next_attack_possible: whether next attack is possible
|
||||
- left: facing direction (raw byte from client)
|
||||
- skill_data: skill data from movement
|
||||
- move_path: movement path data (binary from client)
|
||||
|
||||
Reference: MobPacket.onMove()
|
||||
"""
|
||||
def move_monster(oid, next_attack_possible, left, skill_data, move_path) do
|
||||
Out.new(Opcodes.lp_move_monster())
|
||||
|> Out.encode_int(oid)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_byte(if next_attack_possible, do: 1, else: 0)
|
||||
|> Out.encode_byte(left)
|
||||
|> Out.encode_int(skill_data)
|
||||
|> Out.encode_int(0) # multi target for ball size
|
||||
|> Out.encode_int(0) # rand time for area attack
|
||||
# Movement path (raw binary from client)
|
||||
|> Out.encode_bytes(move_path)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Damage monster packet (LP_MobDamaged).
|
||||
|
||||
## Parameters
|
||||
- oid: monster object ID
|
||||
- damage: damage amount (capped at Integer max)
|
||||
|
||||
Reference: MobPacket.damageMonster()
|
||||
"""
|
||||
def damage_monster(oid, damage) do
|
||||
# Cap damage at max int
|
||||
damage_capped = min(damage, 2_147_483_647)
|
||||
|
||||
Out.new(Opcodes.lp_damage_monster())
|
||||
|> Out.encode_int(oid)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_int(damage_capped)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kill monster packet (LP_MobLeaveField).
|
||||
|
||||
## Parameters
|
||||
- monster: Monster.t() struct
|
||||
- leave_type: how the mob is leaving (0 = remain hp, 1 = etc, 2 = self destruct, etc.)
|
||||
|
||||
Reference: MobPacket.killMonster()
|
||||
"""
|
||||
def kill_monster(monster, leave_type \\ 1) do
|
||||
Out.new(Opcodes.lp_kill_monster())
|
||||
|> Out.encode_int(monster.oid)
|
||||
|> Out.encode_byte(leave_type)
|
||||
# If swallow type, encode swallower character ID
|
||||
|> then(fn packet ->
|
||||
if leave_type == 4 do
|
||||
Out.encode_int(packet, -1)
|
||||
else
|
||||
packet
|
||||
end
|
||||
end)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show monster HP indicator (LP_MobHPIndicator).
|
||||
|
||||
## Parameters
|
||||
- oid: monster object ID
|
||||
- hp_percentage: HP percentage (0-100)
|
||||
|
||||
Reference: MobPacket.showMonsterHP()
|
||||
"""
|
||||
def show_monster_hp(oid, hp_percentage) do
|
||||
Out.new(Opcodes.lp_show_monster_hp())
|
||||
|> Out.encode_int(oid)
|
||||
|> Out.encode_byte(hp_percentage)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show boss HP bar (BOSS_ENV).
|
||||
|
||||
## Parameters
|
||||
- monster: Monster.t() struct
|
||||
|
||||
Reference: MobPacket.showBossHP()
|
||||
"""
|
||||
def show_boss_hp(monster) do
|
||||
# Cap HP at max int for display
|
||||
current_hp = min(monster.hp, 2_147_483_647)
|
||||
max_hp = min(monster.max_hp, 2_147_483_647)
|
||||
|
||||
Out.new(0x9D) # BOSS_ENV opcode
|
||||
|> Out.encode_byte(5)
|
||||
|> Out.encode_int(monster.mob_id)
|
||||
|> Out.encode_int(current_hp)
|
||||
|> Out.encode_int(max_hp)
|
||||
|> Out.encode_byte(6) # Tag color (default)
|
||||
|> Out.encode_byte(5) # Tag bg color (default)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Monster control acknowledgment (LP_MobCtrlAck).
|
||||
|
||||
## Parameters
|
||||
- oid: monster object ID
|
||||
- mob_ctrl_sn: control sequence number
|
||||
- next_attack_possible: whether next attack is possible
|
||||
- mp: monster MP
|
||||
- skill_command: skill command from client
|
||||
- slv: skill level
|
||||
|
||||
Reference: MobPacket.onCtrlAck()
|
||||
"""
|
||||
def mob_ctrl_ack(oid, mob_ctrl_sn, next_attack_possible, mp, skill_command, slv) do
|
||||
Out.new(Opcodes.lp_move_monster_response())
|
||||
|> Out.encode_int(oid)
|
||||
|> Out.encode_short(mob_ctrl_sn)
|
||||
|> Out.encode_byte(if next_attack_possible, do: 1, else: 0)
|
||||
|> Out.encode_short(mp)
|
||||
|> Out.encode_byte(skill_command)
|
||||
|> Out.encode_byte(slv)
|
||||
|> Out.encode_int(0) # forced attack idx
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Reactor Packets
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Spawns a reactor on the map (LP_ReactorEnterField).
|
||||
|
||||
## Parameters
|
||||
- reactor: Reactor.t() struct
|
||||
|
||||
Reference: MaplePacketCreator.spawnReactor()
|
||||
"""
|
||||
def spawn_reactor(reactor) do
|
||||
Out.new(Opcodes.lp_reactor_spawn())
|
||||
|> Out.encode_int(reactor.oid)
|
||||
|> Out.encode_int(reactor.reactor_id)
|
||||
|> Out.encode_byte(reactor.state)
|
||||
|> Out.encode_short(reactor.x)
|
||||
|> Out.encode_short(reactor.y)
|
||||
|> Out.encode_byte(reactor.facing_direction)
|
||||
|> Out.encode_string(reactor.name)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Triggers/hits a reactor (LP_ReactorChangeState).
|
||||
|
||||
## Parameters
|
||||
- reactor: Reactor.t() struct
|
||||
- stance: stance value (usually 0)
|
||||
|
||||
Reference: MaplePacketCreator.triggerReactor()
|
||||
"""
|
||||
def trigger_reactor(reactor, stance \\ 0) do
|
||||
# Cap state for herb/vein reactors (100000-200011 range)
|
||||
state =
|
||||
if reactor.reactor_id >= 100000 and reactor.reactor_id <= 200011 do
|
||||
min(reactor.state, 4)
|
||||
else
|
||||
reactor.state
|
||||
end
|
||||
|
||||
Out.new(Opcodes.lp_reactor_hit())
|
||||
|> Out.encode_int(reactor.oid)
|
||||
|> Out.encode_byte(state)
|
||||
|> Out.encode_short(reactor.x)
|
||||
|> Out.encode_short(reactor.y)
|
||||
|> Out.encode_short(stance)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Destroys/removes a reactor from the map (LP_ReactorLeaveField).
|
||||
|
||||
## Parameters
|
||||
- reactor: Reactor.t() struct
|
||||
|
||||
Reference: MaplePacketCreator.destroyReactor()
|
||||
"""
|
||||
def destroy_reactor(reactor) do
|
||||
Out.new(Opcodes.lp_reactor_destroy())
|
||||
|> Out.encode_int(reactor.oid)
|
||||
|> Out.encode_byte(reactor.state)
|
||||
|> Out.encode_short(reactor.x)
|
||||
|> Out.encode_short(reactor.y)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions for Monster Encoding
|
||||
# ============================================================================
|
||||
|
||||
@doc false
|
||||
defp encode_mob_temporary_stat(packet, monster) do
|
||||
# For GMS v342, encode changed stats first
|
||||
packet
|
||||
|> encode_mob_changed_stats(monster)
|
||||
# Then encode temporary status effects (buffs/debuffs)
|
||||
|> encode_mob_status_mask(monster)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp encode_mob_changed_stats(packet, _monster) do
|
||||
# For GMS: encode byte 1 if stats are changed, 0 if not
|
||||
# For now, assume no changed stats
|
||||
packet
|
||||
|> Out.encode_byte(0)
|
||||
# If changed stats exist, encode:
|
||||
# - hp (int)
|
||||
# - mp (int)
|
||||
# - exp (int)
|
||||
# - watk (int)
|
||||
# - matk (int)
|
||||
# - PDRate (int)
|
||||
# - MDRate (int)
|
||||
# - acc (int)
|
||||
# - eva (int)
|
||||
# - pushed (int)
|
||||
# - level (int)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp encode_mob_status_mask(packet, monster) do
|
||||
# Encode status effects (poison, stun, freeze, etc.)
|
||||
# For now, assume no status effects - send empty mask
|
||||
# Mask is typically 16 integers (64 bytes) for GMS
|
||||
packet
|
||||
|> Out.encode_bytes(<<0::size(16 * 32)-little>>)
|
||||
# If status effects exist, encode for each effect:
|
||||
# - nOption (short)
|
||||
# - rOption (int) - skill ID | skill level << 16
|
||||
# - tOption (short) - duration / 500
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Admin/System Packets
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Drop message packet (system message displayed to player).
|
||||
|
||||
Types:
|
||||
- 0 = Notice (blue)
|
||||
- 1 = Popup (red)
|
||||
- 2 = Megaphone
|
||||
- 3 = Super Megaphone
|
||||
- 4 = Top scrolling message
|
||||
- 5 = System message (yellow)
|
||||
- 6 = Quiz
|
||||
|
||||
Reference: MaplePacketCreator.dropMessage()
|
||||
"""
|
||||
def drop_message(type, message) do
|
||||
Out.new(Opcodes.lp_blow_weather())
|
||||
|> Out.encode_int(type)
|
||||
|> Out.encode_string(message)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Server-wide broadcast message.
|
||||
|
||||
Reference: MaplePacketCreator.serverMessage()
|
||||
"""
|
||||
def server_message(message) do
|
||||
Out.new(Opcodes.lp_event_msg())
|
||||
|> Out.encode_byte(1)
|
||||
|> Out.encode_string(message)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Scrolling server message (top of screen banner).
|
||||
|
||||
Reference: MaplePacketCreator.serverMessage()
|
||||
"""
|
||||
def scrolling_message(message) do
|
||||
Out.new(Opcodes.lp_event_msg())
|
||||
|> Out.encode_byte(4)
|
||||
|> Out.encode_string(message)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Screenshot request packet (admin tool).
|
||||
Sends a session key to the client for screenshot verification.
|
||||
|
||||
Reference: ClientPool.getScreenshot()
|
||||
"""
|
||||
def screenshot_request(session_key) do
|
||||
Out.new(Opcodes.lp_screen_msg())
|
||||
|> Out.encode_long(session_key)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start lie detector on player.
|
||||
|
||||
Reference: MaplePacketCreator.sendLieDetector()
|
||||
"""
|
||||
def start_lie_detector do
|
||||
Out.new(Opcodes.lp_lie_detector())
|
||||
|> Out.encode_byte(1) # Start lie detector
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lie detector result packet.
|
||||
|
||||
Status:
|
||||
- 0 = Success
|
||||
- 1 = Failed (wrong answer)
|
||||
- 2 = Timeout
|
||||
- 3 = Error
|
||||
"""
|
||||
def lie_detector_result(status) do
|
||||
Out.new(Opcodes.lp_lie_detector())
|
||||
|> Out.encode_byte(status)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Admin result packet (command acknowledgment).
|
||||
|
||||
Reference: MaplePacketCreator.getAdminResult()
|
||||
"""
|
||||
def admin_result(success, message) do
|
||||
Out.new(Opcodes.lp_admin_result())
|
||||
|> Out.encode_byte(if success, do: 1, else: 0)
|
||||
|> Out.encode_string(message)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Force disconnect packet (kick player).
|
||||
|
||||
Reference: MaplePacketCreator.getForcedDisconnect()
|
||||
"""
|
||||
def force_disconnect(reason \\ 0) do
|
||||
Out.new(Opcodes.lp_forced_stat_ex())
|
||||
|> Out.encode_byte(reason)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Drop Packets
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Spawns a drop on the map (LP_DropItemFromMapObject).
|
||||
|
||||
## Parameters
|
||||
- drop: Drop.t() struct
|
||||
- source_position: Position where drop originated (monster/player position)
|
||||
- animation: Animation type
|
||||
- 1 = animation (drop falls from source)
|
||||
- 2 = no animation (instant spawn)
|
||||
- 3 = spawn disappearing item [Fade]
|
||||
- 4 = spawn disappearing item
|
||||
- delay: Delay before drop appears (in ms)
|
||||
|
||||
Reference: MaplePacketCreator.dropItemFromMapObject()
|
||||
"""
|
||||
def spawn_drop(drop, source_position \\ nil, animation \\ 1, delay \\ 0) do
|
||||
source_pos = source_position || drop.position
|
||||
|
||||
packet = Out.new(Opcodes.lp_drop_item_from_mapobject())
|
||||
|> Out.encode_byte(animation)
|
||||
|> Out.encode_int(drop.oid)
|
||||
|> Out.encode_byte(if drop.meso > 0, do: 1, else: 0) # 1 = mesos, 0 = item
|
||||
|> Out.encode_int(Drop.display_id(drop))
|
||||
|> Out.encode_int(drop.owner_id)
|
||||
|> Out.encode_byte(drop.drop_type)
|
||||
|> Out.encode_short(drop.position.x)
|
||||
|> Out.encode_short(drop.position.y)
|
||||
|> Out.encode_int(0) # Unknown
|
||||
|
||||
# If animation != 2, encode source position
|
||||
packet = if animation != 2 do
|
||||
packet
|
||||
|> Out.encode_short(source_pos.x)
|
||||
|> Out.encode_short(source_pos.y)
|
||||
|> Out.encode_short(delay)
|
||||
else
|
||||
packet
|
||||
end
|
||||
|
||||
# If not meso, encode expiration time
|
||||
packet = if drop.meso == 0 do
|
||||
# Expiration time - for now, send 0 (no expiration)
|
||||
# In full implementation, this would be the item's expiration timestamp
|
||||
Out.encode_long(packet, 0)
|
||||
else
|
||||
packet
|
||||
end
|
||||
|
||||
# Pet pickup byte
|
||||
# 0 = player can pick up, 1 = pet can pick up
|
||||
packet
|
||||
|> Out.encode_short(if drop.player_drop, do: 0, else: 1)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a drop from the map (LP_RemoveItemFromMap).
|
||||
|
||||
## Parameters
|
||||
- oid: Drop object ID
|
||||
- animation: Removal animation
|
||||
- 0 = Expire/fade out
|
||||
- 1 = Without animation
|
||||
- 2 = Pickup animation
|
||||
- 4 = Explode animation
|
||||
- 5 = Pet pickup
|
||||
- character_id: Character ID performing the action (for pickup animations)
|
||||
- slot: Pet slot (for pet pickup animation)
|
||||
|
||||
Reference: MaplePacketCreator.removeItemFromMap()
|
||||
"""
|
||||
def remove_drop(oid, animation \\ 1, character_id \\ 0, slot \\ 0) do
|
||||
packet = Out.new(Opcodes.lp_remove_item_from_map())
|
||||
|> Out.encode_byte(animation)
|
||||
|> Out.encode_int(oid)
|
||||
|
||||
# If animation >= 2, encode character ID
|
||||
packet = if animation >= 2 do
|
||||
Out.encode_int(packet, character_id)
|
||||
else
|
||||
packet
|
||||
end
|
||||
|
||||
# If animation == 5 (pet pickup), encode slot
|
||||
packet = if animation == 5 do
|
||||
Out.encode_int(packet, slot)
|
||||
else
|
||||
packet
|
||||
end
|
||||
|
||||
Out.to_data(packet)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns multiple drops at once (for explosive drops).
|
||||
Broadcasts a spawn packet for each drop with minimal animation.
|
||||
"""
|
||||
def spawn_drops(drops, source_position, animation \\ 2) do
|
||||
Enum.map(drops, fn drop ->
|
||||
spawn_drop(drop, source_position, animation)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends existing drops to a joining player.
|
||||
"""
|
||||
def send_existing_drops(client_pid, drops) do
|
||||
Enum.each(drops, fn drop ->
|
||||
# Only send drops that haven't been picked up
|
||||
if not drop.picked_up do
|
||||
packet = spawn_drop(drop, nil, 2) # No animation
|
||||
send(client_pid, {:send_packet, packet})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Pet Packets
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Updates pet information in inventory (ModifyInventoryItem).
|
||||
Ported from PetPacket.updatePet()
|
||||
"""
|
||||
def update_pet(pet, item \\ nil, active \\ true) do
|
||||
# Encode inventory update with pet info
|
||||
Out.new(Opcodes.lp_modify_inventory_item())
|
||||
|> Out.encode_byte(0) # Inventory mode
|
||||
|> Out.encode_byte(2) # Update count
|
||||
|> Out.encode_byte(3) # Mode type
|
||||
|> Out.encode_byte(5) # Inventory type (CASH)
|
||||
|> Out.encode_short(pet.inventory_position)
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_byte(5) # Inventory type (CASH)
|
||||
|> Out.encode_short(pet.inventory_position)
|
||||
|> Out.encode_byte(3) # Item type for pet
|
||||
|> Out.encode_int(pet.pet_item_id)
|
||||
|> Out.encode_byte(1)
|
||||
|> Out.encode_long(pet.unique_id)
|
||||
|> encode_pet_item_info(pet, item, active)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns a pet on the map (LP_SpawnPet).
|
||||
Ported from PetPacket.showPet()
|
||||
|
||||
## Parameters
|
||||
- character_id: Owner's character ID
|
||||
- pet: Pet.t() struct
|
||||
- remove: If true, removes the pet
|
||||
- hunger: If true and removing, shows hunger message
|
||||
"""
|
||||
def spawn_pet(character_id, pet, remove \\ false, hunger \\ false) do
|
||||
packet = Out.new(Opcodes.lp_spawn_pet())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode pet slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, pet.summoned)
|
||||
else
|
||||
Out.encode_int(packet, pet.summoned)
|
||||
end
|
||||
|
||||
if remove do
|
||||
packet
|
||||
|> Out.encode_byte(0) # Remove flag
|
||||
|> Out.encode_byte(if hunger, do: 1, else: 0)
|
||||
else
|
||||
packet
|
||||
|> Out.encode_byte(1) # Show flag
|
||||
|> Out.encode_byte(0) # Unknown flag
|
||||
|> Out.encode_int(pet.pet_item_id)
|
||||
|> Out.encode_string(pet.name)
|
||||
|> Out.encode_long(pet.unique_id)
|
||||
|> Out.encode_short(pet.position.x)
|
||||
|> Out.encode_short(pet.position.y - 20) # Offset Y slightly
|
||||
|> Out.encode_byte(pet.stance)
|
||||
|> Out.encode_int(pet.position.fh)
|
||||
end
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a pet from the map.
|
||||
Ported from PetPacket.removePet()
|
||||
"""
|
||||
def remove_pet(character_id, slot) do
|
||||
packet = Out.new(Opcodes.lp_spawn_pet())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, slot)
|
||||
else
|
||||
Out.encode_int(packet, slot)
|
||||
end
|
||||
|
||||
packet
|
||||
|> Out.encode_short(0) # Remove flag
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Moves a pet on the map (LP_PetMove).
|
||||
Ported from PetPacket.movePet()
|
||||
"""
|
||||
def move_pet(character_id, pet_unique_id, slot, movement_data) do
|
||||
packet = Out.new(Opcodes.lp_move_pet())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, slot)
|
||||
else
|
||||
Out.encode_int(packet, slot)
|
||||
end
|
||||
|
||||
packet
|
||||
|> Out.encode_long(pet_unique_id)
|
||||
|> Out.encode_bytes(movement_data)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pet chat packet (LP_PetChat).
|
||||
Ported from PetPacket.petChat()
|
||||
"""
|
||||
def pet_chat(character_id, slot, command, text) do
|
||||
packet = Out.new(Opcodes.lp_pet_chat())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, slot)
|
||||
else
|
||||
Out.encode_int(packet, slot)
|
||||
end
|
||||
|
||||
packet
|
||||
|> Out.encode_short(command)
|
||||
|> Out.encode_string(text)
|
||||
|> Out.encode_byte(0) # hasQuoteRing
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pet command response (LP_PetCommand).
|
||||
Ported from PetPacket.commandResponse()
|
||||
|
||||
## Parameters
|
||||
- character_id: Owner's character ID
|
||||
- slot: Pet slot (0-2)
|
||||
- command: Command ID that was executed
|
||||
- success: Whether the command succeeded
|
||||
- food: Whether this was a food command
|
||||
"""
|
||||
def pet_command_response(character_id, slot, command, success, food) do
|
||||
packet = Out.new(Opcodes.lp_pet_command())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, slot)
|
||||
else
|
||||
Out.encode_int(packet, slot)
|
||||
end
|
||||
|
||||
# Command encoding differs for food
|
||||
packet = if command == 1 do
|
||||
Out.encode_byte(packet, 1)
|
||||
else
|
||||
Out.encode_byte(packet, 0)
|
||||
end
|
||||
|
||||
packet = Out.encode_byte(packet, command)
|
||||
|
||||
packet = if command == 1 do
|
||||
# Food command
|
||||
Out.encode_byte(packet, 0)
|
||||
else
|
||||
# Regular command
|
||||
Out.encode_short(packet, if(success, do: 1, else: 0))
|
||||
end
|
||||
|
||||
Out.to_data(packet)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows own pet level up effect (LP_ShowItemGainInChat).
|
||||
Ported from PetPacket.showOwnPetLevelUp()
|
||||
"""
|
||||
def show_own_pet_level_up(slot) do
|
||||
Out.new(Opcodes.lp_show_item_gain_inchat())
|
||||
|> Out.encode_byte(6) # Type: pet level up
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_int(slot)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows pet level up effect to other players (LP_ShowForeignEffect).
|
||||
Ported from PetPacket.showPetLevelUp()
|
||||
"""
|
||||
def show_pet_level_up(character_id, slot) do
|
||||
Out.new(Opcodes.lp_show_foreign_effect())
|
||||
|> Out.encode_int(character_id)
|
||||
|> Out.encode_byte(6) # Type: pet level up
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_int(slot)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pet name change/update (LP_PetNameChanged).
|
||||
Ported from PetPacket.showPetUpdate()
|
||||
"""
|
||||
def pet_name_change(character_id, pet_unique_id, slot) do
|
||||
packet = Out.new(Opcodes.lp_pet_namechange())
|
||||
|> Out.encode_int(character_id)
|
||||
|
||||
# Encode slot based on GMS mode
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_byte(packet, slot)
|
||||
else
|
||||
Out.encode_int(packet, slot)
|
||||
end
|
||||
|
||||
packet
|
||||
|> Out.encode_long(pet_unique_id)
|
||||
|> Out.encode_byte(0) # Unknown
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pet stat update (UPDATE_STATS with PET flag).
|
||||
Ported from PetPacket.petStatUpdate()
|
||||
"""
|
||||
def pet_stat_update(pets) do
|
||||
# Filter summoned pets
|
||||
summoned_pets = Enum.filter(pets, fn pet -> pet.summoned > 0 end)
|
||||
|
||||
packet = Out.new(Opcodes.lp_update_stats())
|
||||
|> Out.encode_byte(0) # Reset mask flag
|
||||
|
||||
# Encode stat mask based on GMS mode
|
||||
pet_stat_value = if Odinsea.Constants.Game.gms?(), do: 0x200000, else: 0x200000
|
||||
|
||||
packet = if Odinsea.Constants.Game.gms?() do
|
||||
Out.encode_long(packet, pet_stat_value)
|
||||
else
|
||||
Out.encode_int(packet, pet_stat_value)
|
||||
end
|
||||
|
||||
# Encode summoned pet unique IDs (up to 3)
|
||||
packet = Enum.reduce(summoned_pets, packet, fn pet, p ->
|
||||
Out.encode_long(p, pet.unique_id)
|
||||
end)
|
||||
|
||||
# Fill remaining slots with empty
|
||||
remaining = 3 - length(summoned_pets)
|
||||
packet = Enum.reduce(1..remaining, packet, fn _, p ->
|
||||
Out.encode_long(p, 0)
|
||||
end)
|
||||
|
||||
packet
|
||||
|> Out.encode_byte(0)
|
||||
|> Out.encode_short(0)
|
||||
|> Out.to_data()
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Pet Encoding Helpers
|
||||
# ============================================================================
|
||||
|
||||
@doc false
|
||||
defp encode_pet_item_info(packet, pet, item, active) do
|
||||
# Encode full pet item info structure
|
||||
# This includes pet stats, name, level, closeness, fullness, flags, etc.
|
||||
packet = packet
|
||||
|> Out.encode_string(pet.name)
|
||||
|> Out.encode_byte(pet.level)
|
||||
|> Out.encode_short(pet.closeness)
|
||||
|> Out.encode_byte(pet.fullness)
|
||||
|> Out.encode_long(if active, do: pet.unique_id, else: 0)
|
||||
|> Out.encode_short(pet.inventory_position)
|
||||
|> Out.encode_int(pet.seconds_left)
|
||||
|> Out.encode_short(pet.flags)
|
||||
|> Out.encode_int(pet.pet_item_id)
|
||||
|
||||
# Add equip info (3 slots: hat, saddle, decor)
|
||||
# For now, encode empty slots
|
||||
packet
|
||||
|> Out.encode_long(0) # Pet equip slot 1
|
||||
|> Out.encode_long(0) # Pet equip slot 2
|
||||
|> Out.encode_long(0) # Pet equip slot 3
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes pet info for character spawn packet.
|
||||
Called when spawning a player with their active pets.
|
||||
"""
|
||||
def encode_spawn_pets(packet, pets) do
|
||||
# Get summoned pets in slot order
|
||||
summoned_pets = pets
|
||||
|> Enum.filter(fn pet -> pet.summoned > 0 end)
|
||||
|> Enum.sort_by(fn pet -> pet.summoned end)
|
||||
|
||||
# Encode 3 pet slots (0 = no pet)
|
||||
{pet1, rest1} = List.pop_at(summoned_pets, 0, nil)
|
||||
{pet2, rest2} = List.pop_at(rest1, 0, nil)
|
||||
{pet3, _} = List.pop_at(rest2, 0, nil)
|
||||
|
||||
packet
|
||||
|> encode_single_pet_for_spawn(pet1)
|
||||
|> encode_single_pet_for_spawn(pet2)
|
||||
|> encode_single_pet_for_spawn(pet3)
|
||||
end
|
||||
|
||||
defp encode_single_pet_for_spawn(packet, nil) do
|
||||
packet
|
||||
|> Out.encode_byte(0) # No pet
|
||||
end
|
||||
|
||||
defp encode_single_pet_for_spawn(packet, pet) do
|
||||
packet
|
||||
|> Out.encode_byte(1) # Has pet
|
||||
|> Out.encode_int(pet.pet_item_id)
|
||||
|> Out.encode_string(pet.name)
|
||||
|> Out.encode_long(pet.unique_id)
|
||||
|> Out.encode_short(pet.position.x)
|
||||
|> Out.encode_short(pet.position.y)
|
||||
|> Out.encode_byte(pet.stance)
|
||||
|> Out.encode_int(pet.position.fh)
|
||||
|> Out.encode_byte(pet.level)
|
||||
|> Out.encode_short(pet.closeness)
|
||||
|> Out.encode_byte(pet.fullness)
|
||||
|> Out.encode_short(pet.flags)
|
||||
|> Out.encode_int(0) # Pet equip item ID 1
|
||||
|> Out.encode_int(0) # Pet equip item ID 2
|
||||
|> Out.encode_int(0) # Pet equip item ID 3
|
||||
|> Out.encode_int(0) # Pet equip item ID 4
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user