2098 lines
57 KiB
Elixir
2098 lines
57 KiB
Elixir
defmodule Odinsea.Scripting.PlayerAPI do
|
|
@moduledoc """
|
|
Player Interaction API for scripts.
|
|
|
|
This module provides the interface that scripts use to interact with
|
|
players, including dialogue functions, warping, item manipulation, and
|
|
game mechanics.
|
|
|
|
## API Objects
|
|
|
|
Different script types receive this API with different variable names:
|
|
- `cm` - Conversation manager (NPC/Quest scripts)
|
|
- `pi` - Portal interaction (Portal scripts)
|
|
- `rm` - Reactor manager (Reactor scripts)
|
|
- `qm` - Quest manager (Quest scripts - same as cm)
|
|
|
|
## Dialogue Functions
|
|
|
|
### Basic Dialogs
|
|
- `send_ok/1` - Show OK button dialog
|
|
- `send_next/1` - Show Next button dialog
|
|
- `send_prev/1` - Show Previous button dialog
|
|
- `send_next_prev/1` - Show Next and Previous buttons
|
|
|
|
### Choice Dialogs
|
|
- `send_yes_no/1` - Yes/No choice
|
|
- `send_accept_decline/1` - Accept/Decline choice
|
|
- `send_simple/1` - Menu selection with #L tags
|
|
|
|
### Input Dialogs
|
|
- `send_get_text/1` - Text input
|
|
- `send_get_number/4` - Number input with min/max
|
|
- `send_style/2` - Style selection (hair/face)
|
|
- `ask_avatar/2` - Avatar preview
|
|
|
|
### Player-to-NPC Dialogs (Speaker)
|
|
- `send_next_s/2` - Next with speaker type
|
|
- `send_ok_s/2` - OK with speaker type
|
|
- `send_yes_no_s/2` - Yes/No with speaker type
|
|
|
|
## Player Actions
|
|
|
|
### Warping
|
|
- `warp/1` - Warp to map (random portal)
|
|
- `warp/2` - Warp to map with specific portal
|
|
- `warp_instanced/1` - Warp to event instance map
|
|
|
|
### Items
|
|
- `gain_item/2` - Give/take items
|
|
- `gain_item_period/3` - Give item with expiration
|
|
- `have_item/1` - Check if player has item
|
|
- `can_hold/1` - Check if player can hold item
|
|
|
|
### Stats
|
|
- `gain_meso/1` - Give/take meso
|
|
- `gain_exp/1` - Give EXP
|
|
- `change_job/1` - Change job
|
|
- `max_stats/0` - Max all stats (GM)
|
|
|
|
## Example Usage
|
|
|
|
def start(cm) do
|
|
PlayerAPI.send_next(cm, "Hello there!")
|
|
end
|
|
|
|
def action(cm, mode, type, selection) do
|
|
if mode == 1 do
|
|
PlayerAPI.send_yes_no(cm, "Would you like to go to Henesys?")
|
|
else
|
|
PlayerAPI.dispose(cm)
|
|
end
|
|
end
|
|
"""
|
|
|
|
require Logger
|
|
import Bitwise
|
|
|
|
alias Odinsea.Game.{Character, Inventory, Item, Map}
|
|
alias Odinsea.Game.{LifeFactory, Monster}
|
|
alias Odinsea.Channel.Packets
|
|
alias Odinsea.Net.Packet.Out
|
|
alias Odinsea.Net.Opcodes
|
|
|
|
# Message type codes (matching Java implementation)
|
|
@msg_ok 0
|
|
@msg_next 0
|
|
@msg_prev 0
|
|
@msg_next_prev 0
|
|
@msg_yes_no 2
|
|
@msg_get_text 3
|
|
@msg_get_number 4
|
|
@msg_simple 5
|
|
@msg_accept_decline 0x0E
|
|
@msg_style 9
|
|
|
|
# Default channel for map operations
|
|
@default_channel 1
|
|
|
|
# ============================================================================
|
|
# Types
|
|
# ============================================================================
|
|
|
|
@type t :: %__MODULE__{
|
|
client_pid: pid(),
|
|
character_id: integer(),
|
|
npc_id: integer(),
|
|
quest_id: integer() | nil,
|
|
manager_pid: pid() | nil,
|
|
get_text: String.t() | nil
|
|
}
|
|
|
|
defstruct [
|
|
:client_pid,
|
|
:character_id,
|
|
:npc_id,
|
|
:quest_id,
|
|
:manager_pid,
|
|
:get_text
|
|
]
|
|
|
|
# ============================================================================
|
|
# Constructor
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Creates a new Player API instance.
|
|
|
|
## Parameters
|
|
- `client_pid` - Client process PID
|
|
- `character_id` - Character ID
|
|
- `npc_id` - NPC ID (or portal/reactor ID)
|
|
- `quest_id` - Quest ID (nil for non-quest)
|
|
- `manager_pid` - NPCManager PID (for disposal)
|
|
"""
|
|
@spec new(pid(), integer(), integer(), integer() | nil, pid() | nil) :: t()
|
|
def new(client_pid, character_id, npc_id, quest_id \\ nil, manager_pid \\ nil) do
|
|
%__MODULE__{
|
|
client_pid: client_pid,
|
|
character_id: character_id,
|
|
npc_id: npc_id,
|
|
quest_id: quest_id,
|
|
manager_pid: manager_pid,
|
|
get_text: nil
|
|
}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Conversation Control
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Ends the conversation.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
"""
|
|
@spec dispose(t()) :: :ok
|
|
def dispose(%__MODULE__{manager_pid: nil}), do: :ok
|
|
def dispose(%__MODULE__{manager_pid: pid, client_pid: cpid, character_id: cid}) do
|
|
Odinsea.Scripting.NPCManager.dispose(cpid, cid)
|
|
end
|
|
|
|
@doc """
|
|
Schedules disposal on the next action.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
"""
|
|
@spec safe_dispose(t()) :: :ok
|
|
def safe_dispose(%__MODULE__{manager_pid: nil}), do: :ok
|
|
def safe_dispose(%__MODULE__{client_pid: cpid, character_id: cid}) do
|
|
Odinsea.Scripting.NPCManager.safe_dispose(cpid, cid)
|
|
end
|
|
|
|
@doc """
|
|
Sets the last message type for validation.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `msg_type` - Message type code
|
|
"""
|
|
@spec set_last_msg(t(), integer()) :: :ok
|
|
def set_last_msg(%__MODULE__{client_pid: cpid, character_id: cid}, msg_type) do
|
|
Odinsea.Scripting.NPCManager.set_last_msg(cpid, cid, msg_type)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Basic Dialogs
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sends an OK dialog.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Dialog text (can include #b#k tags)
|
|
"""
|
|
@spec send_ok(t(), String.t()) :: :ok
|
|
def send_ok(api, text) do
|
|
send_ok_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends an OK dialog with specific NPC ID.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Dialog text
|
|
- `npc_id` - NPC ID to display
|
|
"""
|
|
@spec send_ok_npc(t(), String.t(), integer()) :: :ok
|
|
def send_ok_npc(api, text, npc_id) do
|
|
packet = npc_talk_packet(npc_id, 0, text, "00 00", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} says (OK): #{text}")
|
|
set_last_msg(api, @msg_ok)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a Next dialog (user clicks Next to continue).
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Dialog text
|
|
"""
|
|
@spec send_next(t(), String.t()) :: :ok
|
|
def send_next(api, text) do
|
|
send_next_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a Next dialog with specific NPC ID.
|
|
"""
|
|
@spec send_next_npc(t(), String.t(), integer()) :: :ok
|
|
def send_next_npc(api, text, npc_id) do
|
|
# Check for #L tags (would DC otherwise)
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
# TODO: Send NPCTalk packet with "00 01" style
|
|
Logger.debug("NPC #{npc_id} says (Next): #{text}")
|
|
set_last_msg(api, @msg_next)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a Previous dialog.
|
|
"""
|
|
@spec send_prev(t(), String.t()) :: :ok
|
|
def send_prev(api, text) do
|
|
send_prev_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a Previous dialog with specific NPC ID.
|
|
"""
|
|
@spec send_prev_npc(t(), String.t(), integer()) :: :ok
|
|
def send_prev_npc(api, text, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
packet = npc_talk_packet(npc_id, 0, text, "01 00", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} says (Prev): #{text}")
|
|
set_last_msg(api, @msg_prev)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a Next/Previous dialog.
|
|
"""
|
|
@spec send_next_prev(t(), String.t()) :: :ok
|
|
def send_next_prev(api, text) do
|
|
send_next_prev_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a Next/Previous dialog with specific NPC ID.
|
|
"""
|
|
@spec send_next_prev_npc(t(), String.t(), integer()) :: :ok
|
|
def send_next_prev_npc(api, text, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
packet = npc_talk_packet(npc_id, 0, text, "01 01", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} says (Next/Prev): #{text}")
|
|
set_last_msg(api, @msg_next_prev)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Speaker Dialogs (Player-to-NPC)
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sends a Next dialog with speaker.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Dialog text
|
|
- `speaker_type` - Speaker type (0=NPC, 1=No ESC, 3=Player)
|
|
"""
|
|
@spec send_next_s(t(), String.t(), integer()) :: :ok
|
|
def send_next_s(api, text, speaker_type) do
|
|
send_next_s_npc(api, text, speaker_type, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a Next dialog with speaker and specific NPC ID.
|
|
"""
|
|
@spec send_next_s_npc(t(), String.t(), integer(), integer()) :: :ok
|
|
def send_next_s_npc(api, text, speaker_type, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_s_npc(api, text, speaker_type, npc_id)
|
|
else
|
|
packet = npc_talk_packet(api.npc_id, 0, text, "00 01", speaker_type, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} says (NextS, type=#{speaker_type}): #{text}")
|
|
set_last_msg(api, @msg_next)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends an OK dialog with speaker.
|
|
"""
|
|
@spec send_ok_s(t(), String.t(), integer()) :: :ok
|
|
def send_ok_s(api, text, speaker_type) do
|
|
packet = npc_talk_packet(api.npc_id, 0, text, "00 00", speaker_type, api.npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} says (OkS, type=#{speaker_type}): #{text}")
|
|
set_last_msg(api, @msg_ok)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a Yes/No dialog with speaker.
|
|
"""
|
|
@spec send_yes_no_s(t(), String.t(), integer()) :: :ok
|
|
def send_yes_no_s(api, text, speaker_type) do
|
|
packet = npc_talk_packet(api.npc_id, 2, text, "", speaker_type, api.npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} asks (YesNoS, type=#{speaker_type}): #{text}")
|
|
set_last_msg(api, @msg_yes_no)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a Player-to-NPC dialog (convenience).
|
|
"""
|
|
@spec player_to_npc(t(), String.t()) :: :ok
|
|
def player_to_npc(api, text) do
|
|
send_next_s(api, text, 3)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Choice Dialogs
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sends a Yes/No dialog.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Question text
|
|
"""
|
|
@spec send_yes_no(t(), String.t()) :: :ok
|
|
def send_yes_no(api, text) do
|
|
send_yes_no_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a Yes/No dialog with specific NPC ID.
|
|
"""
|
|
@spec send_yes_no_npc(t(), String.t(), integer()) :: :ok
|
|
def send_yes_no_npc(api, text, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
packet = npc_talk_packet(npc_id, 2, text, "", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} asks (Yes/No): #{text}")
|
|
set_last_msg(api, @msg_yes_no)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends an Accept/Decline dialog.
|
|
"""
|
|
@spec send_accept_decline(t(), String.t()) :: :ok
|
|
def send_accept_decline(api, text) do
|
|
send_accept_decline_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends an Accept/Decline dialog with specific NPC ID.
|
|
"""
|
|
@spec send_accept_decline_npc(t(), String.t(), integer()) :: :ok
|
|
def send_accept_decline_npc(api, text, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
# Type 0x0E (14) for Accept/Decline
|
|
packet = npc_talk_packet(npc_id, 0x0E, text, "", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} asks (Accept/Decline): #{text}")
|
|
set_last_msg(api, @msg_accept_decline)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a simple menu dialog.
|
|
|
|
Text should contain #L tags for menu items:
|
|
`#L0#Option 1#l\r\n#L1#Option 2#l`
|
|
"""
|
|
@spec send_simple(t(), String.t()) :: :ok
|
|
def send_simple(api, text) do
|
|
send_simple_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a simple menu dialog with specific NPC ID.
|
|
"""
|
|
@spec send_simple_npc(t(), String.t(), integer()) :: :ok
|
|
def send_simple_npc(api, text, npc_id) do
|
|
if not String.contains?(text, "#L") do
|
|
# Would DC otherwise
|
|
send_next_npc(api, text, npc_id)
|
|
else
|
|
packet = npc_talk_packet(npc_id, 5, text, "", 0, npc_id)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} menu: #{text}")
|
|
set_last_msg(api, @msg_simple)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a simple menu with selection array.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Intro text
|
|
- `selections` - List of menu options
|
|
"""
|
|
@spec send_simple_selections(t(), String.t(), [String.t()]) :: :ok
|
|
def send_simple_selections(api, text, selections) do
|
|
menu_text =
|
|
if length(selections) > 0 do
|
|
text <> "#b\r\n" <> build_menu(selections)
|
|
else
|
|
text
|
|
end
|
|
|
|
send_simple(api, menu_text)
|
|
end
|
|
|
|
defp build_menu(selections) do
|
|
selections
|
|
|> Enum.with_index()
|
|
|> Enum.map_join("\r\n", fn {item, idx} ->
|
|
"#L#{idx}##{item}#l"
|
|
end)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Input Dialogs
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sends a text input dialog.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Prompt text
|
|
"""
|
|
@spec send_get_text(t(), String.t()) :: :ok
|
|
def send_get_text(api, text) do
|
|
send_get_text_npc(api, text, api.npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Sends a text input dialog with specific NPC ID.
|
|
"""
|
|
@spec send_get_text_npc(t(), String.t(), integer()) :: :ok
|
|
def send_get_text_npc(api, text, npc_id) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple_npc(api, text, npc_id)
|
|
else
|
|
packet = npc_talk_text_packet(npc_id, text, 0, 0, "")
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{npc_id} asks for text: #{text}")
|
|
set_last_msg(api, @msg_get_text)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a text input dialog with constraints.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Prompt text
|
|
- `min` - Minimum length
|
|
- `max` - Maximum length
|
|
- `default` - Default text
|
|
"""
|
|
@spec send_get_text_constrained(t(), String.t(), integer(), integer(), String.t()) :: :ok
|
|
def send_get_text_constrained(api, text, min, max, default) do
|
|
packet = npc_talk_text_packet(api.npc_id, text, min, max, default)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} asks for text (#{min}-#{max}): #{text}")
|
|
set_last_msg(api, @msg_get_text)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a number input dialog.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Prompt text
|
|
- `default` - Default value
|
|
- `min` - Minimum value
|
|
- `max` - Maximum value
|
|
"""
|
|
@spec send_get_number(t(), String.t(), integer(), integer(), integer()) :: :ok
|
|
def send_get_number(api, text, default, min, max) do
|
|
if String.contains?(text, "#L") do
|
|
send_simple(api, text)
|
|
else
|
|
packet = npc_talk_num_packet(api.npc_id, text, default, min, max)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} asks for number (#{min}-#{max}, default #{default}): #{text}")
|
|
set_last_msg(api, @msg_get_number)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a style selection dialog (for hair/face).
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - Prompt text
|
|
- `styles` - List of style IDs
|
|
"""
|
|
@spec send_style(t(), String.t(), [integer()]) :: :ok
|
|
def send_style(api, text, styles) do
|
|
send_style_paged(api, text, styles, 0)
|
|
end
|
|
|
|
@doc """
|
|
Sends a style selection dialog with page.
|
|
"""
|
|
@spec send_style_paged(t(), String.t(), [integer()], integer()) :: :ok
|
|
def send_style_paged(api, text, styles, page) do
|
|
packet = npc_talk_style_packet(api.npc_id, text, styles, page)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} style selection (page #{page}): #{length(styles)} options")
|
|
set_last_msg(api, @msg_style)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends an avatar selection dialog.
|
|
"""
|
|
@spec ask_avatar(t(), String.t(), [integer()]) :: :ok
|
|
def ask_avatar(api, text, styles) do
|
|
send_style_paged(api, text, styles, 0)
|
|
end
|
|
|
|
@doc """
|
|
Sends a map selection dialog.
|
|
"""
|
|
@spec ask_map_selection(t(), String.t()) :: :ok
|
|
def ask_map_selection(api, selection_string) do
|
|
packet = map_selection_packet(api.npc_id, selection_string)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("NPC #{api.npc_id} map selection")
|
|
set_last_msg(api, 0x10)
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Get/Set Text
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sets the text received from player input.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `text` - The text value
|
|
"""
|
|
@spec set_get_text(t(), String.t()) :: t()
|
|
def set_get_text(%__MODULE__{} = api, text) do
|
|
%{api | get_text: text}
|
|
end
|
|
|
|
@doc """
|
|
Gets the text received from player input.
|
|
"""
|
|
@spec get_get_text(t()) :: String.t() | nil
|
|
def get_get_text(%__MODULE__{get_text: text}), do: text
|
|
|
|
# ============================================================================
|
|
# Character Appearance
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sets the character's hair style.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `hair` - Hair style ID
|
|
"""
|
|
@spec set_hair(t(), integer()) :: :ok
|
|
def set_hair(api, hair) do
|
|
# Update character hair through Character module
|
|
case Character.get_state(api.character_id) do
|
|
nil ->
|
|
Logger.warn("Character #{api.character_id} not found for set_hair")
|
|
%Character.State{} = state ->
|
|
# Send stat update packet
|
|
packet = update_stat_packet([{:hair, hair}], state.job)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("Set hair to #{hair} for character #{api.character_id}")
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sets the character's face.
|
|
"""
|
|
@spec set_face(t(), integer()) :: :ok
|
|
def set_face(api, face) do
|
|
case Character.get_state(api.character_id) do
|
|
nil ->
|
|
Logger.warn("Character #{api.character_id} not found for set_face")
|
|
%Character.State{} = state ->
|
|
packet = update_stat_packet([{:face, face}], state.job)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("Set face to #{face} for character #{api.character_id}")
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sets the character's skin color.
|
|
"""
|
|
@spec set_skin(t(), integer()) :: :ok
|
|
def set_skin(api, color) do
|
|
case Character.get_state(api.character_id) do
|
|
nil ->
|
|
Logger.warn("Character #{api.character_id} not found for set_skin")
|
|
%Character.State{} = state ->
|
|
packet = update_stat_packet([{:skin, color}], state.job)
|
|
send_packet(api.client_pid, packet)
|
|
Logger.debug("Set skin color to #{color} for character #{api.character_id}")
|
|
end
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sets a random avatar from given options.
|
|
"""
|
|
@spec set_random_avatar(t(), integer(), [integer()]) :: integer()
|
|
def set_random_avatar(api, ticket, options) do
|
|
if have_item(api, ticket) do
|
|
gain_item(api, ticket, -1)
|
|
style = Enum.random(options)
|
|
|
|
cond do
|
|
style < 100 -> set_skin(api, style)
|
|
style < 30000 -> set_face(api, style)
|
|
true -> set_hair(api, style)
|
|
end
|
|
|
|
1
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sets a specific avatar style.
|
|
"""
|
|
@spec set_avatar(t(), integer(), integer()) :: integer()
|
|
def set_avatar(api, ticket, style) do
|
|
if have_item(api, ticket) do
|
|
gain_item(api, ticket, -1)
|
|
|
|
cond do
|
|
style < 100 -> set_skin(api, style)
|
|
style < 30000 -> set_face(api, style)
|
|
true -> set_hair(api, style)
|
|
end
|
|
|
|
1
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Warping
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Warps the player to a map.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `map_id` - Target map ID
|
|
"""
|
|
@spec warp(t(), integer()) :: :ok
|
|
def warp(api, map_id) do
|
|
# Use random portal
|
|
warp_portal(api, map_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Warps the player to a map with specific portal.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `map_id` - Target map ID
|
|
- `portal` - Portal ID or name
|
|
"""
|
|
@spec warp_portal(t(), integer(), integer() | String.t()) :: :ok
|
|
def warp_portal(api, map_id, portal) do
|
|
Logger.debug("Warping character #{api.character_id} to map #{map_id}, portal #{inspect(portal)}")
|
|
|
|
# Use Character.change_map which handles the map transition
|
|
spawn_point = if is_integer(portal), do: portal, else: 0
|
|
Character.change_map(api.character_id, map_id, spawn_point)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Warps the player to an instanced map.
|
|
"""
|
|
@spec warp_instanced(t(), integer()) :: :ok
|
|
def warp_instanced(api, map_id) do
|
|
Logger.debug("Warping character #{api.character_id} to instanced map #{map_id}")
|
|
# TODO: Handle event instance maps
|
|
Character.change_map(api.character_id, map_id, 0)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Warps all players on the current map.
|
|
"""
|
|
@spec warp_map(t(), integer(), integer()) :: :ok
|
|
def warp_map(api, map_id, portal) do
|
|
Logger.debug("Warping all players on current map to map #{map_id}")
|
|
|
|
# Get current map and warp all players
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: current_map_id} ->
|
|
# Get all players on current map
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
players = Map.get_players(current_map_id, channel_id)
|
|
|
|
Enum.each(players, fn {char_id, _} ->
|
|
if char_id != api.character_id do
|
|
Character.change_map(char_id, map_id, portal)
|
|
end
|
|
end)
|
|
|
|
# Also warp self
|
|
Character.change_map(api.character_id, map_id, portal)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Warps the entire party.
|
|
"""
|
|
@spec warp_party(t(), integer()) :: :ok
|
|
def warp_party(api, map_id) do
|
|
Logger.debug("Warping party to map #{map_id}")
|
|
|
|
# TODO: Get party members and warp them
|
|
# For now, just warp self
|
|
Character.change_map(api.character_id, map_id, 0)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Plays the portal sound effect.
|
|
"""
|
|
@spec play_portal_se(t()) :: :ok
|
|
def play_portal_se(api) do
|
|
# Send show effect packet for portal sound
|
|
# Effect type 7 is the portal sound effect
|
|
packet = show_own_buff_effect_packet(0, 7, 1, 1)
|
|
send_packet(api.client_pid, packet)
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Items
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Gives or takes items from the player.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `item_id` - Item ID
|
|
- `quantity` - Positive to give, negative to take
|
|
"""
|
|
@spec gain_item(t(), integer(), integer()) :: :ok
|
|
def gain_item(api, item_id, quantity) do
|
|
gain_item_full(api, item_id, quantity, false, 0, -1, "")
|
|
end
|
|
|
|
@doc """
|
|
Gives items with random stats.
|
|
"""
|
|
@spec gain_item_random(t(), integer(), integer(), boolean()) :: :ok
|
|
def gain_item_random(api, item_id, quantity, random_stats) do
|
|
gain_item_full(api, item_id, quantity, random_stats, 0, -1, "")
|
|
end
|
|
|
|
@doc """
|
|
Gives items with slot bonus.
|
|
"""
|
|
@spec gain_item_slots(t(), integer(), integer(), boolean(), integer()) :: :ok
|
|
def gain_item_slots(api, item_id, quantity, random_stats, slots) do
|
|
gain_item_full(api, item_id, quantity, random_stats, 0, slots, "")
|
|
end
|
|
|
|
@doc """
|
|
Gives items with expiration period.
|
|
"""
|
|
@spec gain_item_period(t(), integer(), integer(), integer()) :: :ok
|
|
def gain_item_period(api, item_id, quantity, days) do
|
|
gain_item_full(api, item_id, quantity, false, days, -1, "")
|
|
end
|
|
|
|
@doc """
|
|
Gives items with full options.
|
|
"""
|
|
@spec gain_item_full(t(), integer(), integer(), boolean(), integer(), integer(), String.t()) :: :ok
|
|
def gain_item_full(api, item_id, quantity, random_stats, period, slots, owner) do
|
|
Logger.debug("Gain item #{item_id} x#{quantity} (random=#{random_stats}, period=#{period})")
|
|
|
|
if quantity >= 0 do
|
|
# Add item to inventory
|
|
inventory_type = Inventory.get_type_by_item_id(item_id)
|
|
|
|
# Get or create item
|
|
item = if inventory_type == :equip do
|
|
# For equipment, create with random stats if requested
|
|
Item.new_equip(item_id, random_stats)
|
|
else
|
|
# For regular items
|
|
Item.new(item_id, quantity)
|
|
end
|
|
|
|
# Set expiration if period > 0
|
|
item = if period > 0 do
|
|
expire_time = System.system_time(:millisecond) + (period * 24 * 60 * 60 * 1000)
|
|
%{item | expiration: expire_time}
|
|
else
|
|
item
|
|
end
|
|
|
|
# Set owner if provided
|
|
item = if owner != "" do
|
|
%{item | owner: owner}
|
|
else
|
|
item
|
|
end
|
|
|
|
# Add slots if specified (for equipment)
|
|
item = if slots > 0 and inventory_type == :equip do
|
|
%{item | upgrade_slots: item.upgrade_slots + slots}
|
|
else
|
|
item
|
|
end
|
|
|
|
# Add to inventory
|
|
case Character.add_item(api.character_id, inventory_type, item) do
|
|
:ok ->
|
|
# Send item gain packet
|
|
packet = Packets.show_item_gain(item_id, quantity, true)
|
|
send_packet(api.client_pid, packet)
|
|
:ok
|
|
{:error, reason} ->
|
|
Logger.warn("Failed to add item #{item_id}: #{inspect(reason)}")
|
|
:ok
|
|
end
|
|
else
|
|
# Remove item (negative quantity)
|
|
remove_count = -quantity
|
|
case Character.remove_item_by_id(api.character_id, item_id, remove_count) do
|
|
:ok ->
|
|
packet = Packets.show_item_gain(item_id, quantity, true)
|
|
send_packet(api.client_pid, packet)
|
|
:ok
|
|
{:error, reason} ->
|
|
Logger.warn("Failed to remove item #{item_id}: #{inspect(reason)}")
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if player has an item.
|
|
"""
|
|
@spec have_item(t(), integer()) :: boolean()
|
|
def have_item(api, item_id) do
|
|
have_item_count(api, item_id, 1)
|
|
end
|
|
|
|
@doc """
|
|
Checks if player has at least quantity of an item.
|
|
"""
|
|
@spec have_item_count(t(), integer(), integer()) :: boolean()
|
|
def have_item_count(api, item_id, quantity) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> false
|
|
%Character.State{inventories: inventories} ->
|
|
inventory_type = Inventory.get_type_by_item_id(item_id)
|
|
inventory = Map.get(inventories, inventory_type, Inventory.new(inventory_type))
|
|
Inventory.has_item_count(inventory, item_id, quantity)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Removes an item from inventory.
|
|
"""
|
|
@spec remove_item(t(), integer()) :: boolean()
|
|
def remove_item(api, item_id) do
|
|
case Character.remove_item_by_id(api.character_id, item_id, 1) do
|
|
:ok ->
|
|
packet = Packets.show_item_gain(item_id, -1, true)
|
|
send_packet(api.client_pid, packet)
|
|
true
|
|
{:error, _reason} ->
|
|
false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if player can hold an item.
|
|
"""
|
|
@spec can_hold(t(), integer()) :: boolean()
|
|
def can_hold(api, item_id) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> false
|
|
%Character.State{inventories: inventories} ->
|
|
inventory_type = Inventory.get_type_by_item_id(item_id)
|
|
inventory = Map.get(inventories, inventory_type, Inventory.new(inventory_type))
|
|
Inventory.has_free_slot(inventory)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if player can hold quantity of an item.
|
|
"""
|
|
@spec can_hold_quantity(t(), integer(), integer()) :: boolean()
|
|
def can_hold_quantity(api, item_id, quantity) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> false
|
|
%Character.State{inventories: inventories} ->
|
|
inventory_type = Inventory.get_type_by_item_id(item_id)
|
|
inventory = Map.get(inventories, inventory_type, Inventory.new(inventory_type))
|
|
Inventory.can_hold_quantity(inventory, item_id, quantity)
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Meso/EXP
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Gives or takes meso.
|
|
|
|
## Parameters
|
|
- `api` - The API struct
|
|
- `amount` - Positive to give, negative to take
|
|
"""
|
|
@spec gain_meso(t(), integer()) :: :ok
|
|
def gain_meso(api, amount) do
|
|
Logger.debug("Gain meso: #{amount} for character #{api.character_id}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil ->
|
|
Logger.warn("Character #{api.character_id} not found for gain_meso")
|
|
%Character.State{meso: current_meso} ->
|
|
new_meso = max(0, current_meso + amount)
|
|
|
|
# Update character meso
|
|
Character.update_meso(api.character_id, new_meso)
|
|
|
|
# Send meso update packet
|
|
packet = update_stat_packet([{:meso, new_meso}], 0)
|
|
send_packet(api.client_pid, packet)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the player's current meso.
|
|
"""
|
|
@spec get_meso(t()) :: integer()
|
|
def get_meso(api) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> 0
|
|
%Character.State{meso: meso} -> meso
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gives EXP to the player.
|
|
"""
|
|
@spec gain_exp(t(), integer()) :: :ok
|
|
def gain_exp(api, amount) do
|
|
Logger.debug("Gain EXP: #{amount} for character #{api.character_id}")
|
|
Character.gain_exp(api.character_id, amount, false)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gives EXP with rate multiplier.
|
|
"""
|
|
@spec gain_exp_r(t(), integer()) :: :ok
|
|
def gain_exp_r(api, amount) do
|
|
# TODO: Apply rate multiplier
|
|
gain_exp(api, amount)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Jobs and Skills
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Changes the player's job.
|
|
"""
|
|
@spec change_job(t(), integer()) :: :ok
|
|
def change_job(api, job_id) do
|
|
Logger.debug("Change job to #{job_id} for character #{api.character_id}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil ->
|
|
Logger.warn("Character #{api.character_id} not found for change_job")
|
|
%Character.State{} = state ->
|
|
# Update job
|
|
Character.update_job(api.character_id, job_id)
|
|
|
|
# Send job update packet
|
|
packet = update_stat_packet([{:job, job_id}], job_id)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
# Show job change effect
|
|
effect_packet = show_foreign_effect_packet(api.character_id, 8)
|
|
# Broadcast to map
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
Map.broadcast(state.map_id, channel_id, effect_packet)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the player's current job.
|
|
"""
|
|
@spec get_job(t()) :: integer()
|
|
def get_job(api) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> 0
|
|
%Character.State{job: job} -> job
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Teaches a skill to the player.
|
|
"""
|
|
@spec teach_skill(t(), integer(), integer(), integer()) :: :ok
|
|
def teach_skill(api, skill_id, level, master_level) do
|
|
Logger.debug("Teach skill #{skill_id} level #{level}/#{master_level} to character #{api.character_id}")
|
|
|
|
# Update skill through Character module
|
|
Character.update_skill(api.character_id, skill_id, level, master_level)
|
|
|
|
# Send skill update packet
|
|
packet = update_skill_packet(skill_id, level, master_level, System.system_time(:millisecond))
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Teaches a skill with max master level.
|
|
"""
|
|
@spec teach_skill_max(t(), integer(), integer()) :: :ok
|
|
def teach_skill_max(api, skill_id, level) do
|
|
# TODO: Get max level from skill data
|
|
teach_skill(api, skill_id, level, 20)
|
|
end
|
|
|
|
@doc """
|
|
Checks if player has a skill.
|
|
"""
|
|
@spec has_skill(t(), integer()) :: boolean()
|
|
def has_skill(api, skill_id) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> false
|
|
%Character.State{skills: skills} ->
|
|
case Map.get(skills, skill_id) do
|
|
nil -> false
|
|
%{level: level} -> level > 0
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Maxes all skills for the player (GM).
|
|
"""
|
|
@spec max_all_skills(t()) :: :ok
|
|
def max_all_skills(api) do
|
|
Logger.debug("Max all skills for character #{api.character_id}")
|
|
# TODO: Get all skills for current job and max them
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Stats
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Maxes all stats for the player (GM).
|
|
"""
|
|
@spec max_stats(t()) :: :ok
|
|
def max_stats(_api) do
|
|
Logger.debug("Max stats")
|
|
# TODO: Set all stats to max
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Adds AP (ability points).
|
|
"""
|
|
@spec gain_ap(t(), integer()) :: :ok
|
|
def gain_ap(_api, amount) do
|
|
Logger.debug("Gain AP: #{amount}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Resets stats to specified values.
|
|
"""
|
|
@spec reset_stats(t(), integer(), integer(), integer(), integer()) :: :ok
|
|
def reset_stats(_api, str, dex, int, luk) do
|
|
Logger.debug("Reset stats to STR=#{str} DEX=#{dex} INT=#{int} LUK=#{luk}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets a player stat by name.
|
|
|
|
Valid stat names: "LVL", "STR", "DEX", "INT", "LUK", "HP", "MP",
|
|
"MAXHP", "MAXMP", "RAP", "RSP", "GID", "GRANK", "ARANK", "GM", "ADMIN",
|
|
"GENDER", "FACE", "HAIR"
|
|
"""
|
|
@spec get_player_stat(t(), String.t()) :: integer()
|
|
def get_player_stat(_api, stat_name) do
|
|
Logger.debug("Get stat: #{stat_name}")
|
|
# TODO: Return stat value
|
|
case stat_name do
|
|
"LVL" -> 1
|
|
"GM" -> 0
|
|
"ADMIN" -> 0
|
|
_ -> 0
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Quests
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Starts a quest.
|
|
"""
|
|
@spec start_quest(t(), integer()) :: :ok
|
|
def start_quest(api, quest_id) do
|
|
Logger.debug("Start quest #{quest_id} for character #{api.character_id}")
|
|
|
|
# Start quest through Quest module
|
|
Odinsea.Game.Quest.start_quest(api.character_id, quest_id, api.npc_id)
|
|
|
|
# Send quest start packet
|
|
packet = quest_operation_packet(quest_id, 1)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Completes a quest.
|
|
"""
|
|
@spec complete_quest(t(), integer()) :: :ok
|
|
def complete_quest(api, quest_id) do
|
|
Logger.debug("Complete quest #{quest_id} for character #{api.character_id}")
|
|
|
|
# Complete quest through Quest module
|
|
Odinsea.Game.Quest.complete_quest(api.character_id, quest_id, api.npc_id)
|
|
|
|
# Send quest complete packet
|
|
packet = quest_operation_packet(quest_id, 2)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Forfeits a quest.
|
|
"""
|
|
@spec forfeit_quest(t(), integer()) :: :ok
|
|
def forfeit_quest(api, quest_id) do
|
|
Logger.debug("Forfeit quest #{quest_id} for character #{api.character_id}")
|
|
|
|
# Forfeit quest through Quest module
|
|
Odinsea.Game.Quest.forfeit_quest(api.character_id, quest_id)
|
|
|
|
# Send quest forfeit packet
|
|
packet = quest_operation_packet(quest_id, 0)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Forces a quest start.
|
|
"""
|
|
@spec force_start_quest(t(), integer()) :: :ok
|
|
def force_start_quest(_api, quest_id) do
|
|
Logger.debug("Force start quest #{quest_id}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Forces a quest complete.
|
|
"""
|
|
@spec force_complete_quest(t(), integer()) :: :ok
|
|
def force_complete_quest(_api, quest_id) do
|
|
Logger.debug("Force complete quest #{quest_id}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets quest custom data.
|
|
"""
|
|
@spec get_quest_custom_data(t()) :: String.t() | nil
|
|
def get_quest_custom_data(_api) do
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Sets quest custom data.
|
|
"""
|
|
@spec set_quest_custom_data(t(), String.t()) :: :ok
|
|
def set_quest_custom_data(_api, data) do
|
|
Logger.debug("Set quest custom data: #{data}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets quest record.
|
|
"""
|
|
@spec get_quest_record(t(), integer()) :: term()
|
|
def get_quest_record(_api, _quest_id) do
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Gets quest status (0 = not started, 1 = in progress, 2 = completed).
|
|
"""
|
|
@spec get_quest_status(t(), integer()) :: integer()
|
|
def get_quest_status(api, quest_id) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> 0
|
|
%Character.State{} ->
|
|
Odinsea.Game.Quest.get_status(api.character_id, quest_id)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if quest is active.
|
|
"""
|
|
@spec is_quest_active(t(), integer()) :: boolean()
|
|
def is_quest_active(api, quest_id) do
|
|
get_quest_status(api, quest_id) == 1
|
|
end
|
|
|
|
@doc """
|
|
Checks if quest is finished.
|
|
"""
|
|
@spec is_quest_finished(t(), integer()) :: boolean()
|
|
def is_quest_finished(api, quest_id) do
|
|
get_quest_status(api, quest_id) == 2
|
|
end
|
|
|
|
# ============================================================================
|
|
# Map/Mob Operations
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Gets the current map ID.
|
|
"""
|
|
@spec get_map_id(t()) :: integer()
|
|
def get_map_id(api) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> 100000000
|
|
%Character.State{map_id: map_id} -> map_id
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets map reference.
|
|
"""
|
|
@spec get_map(t(), integer()) :: term()
|
|
def get_map(_api, map_id) do
|
|
# TODO: Get map instance
|
|
%{id: map_id}
|
|
end
|
|
|
|
@doc """
|
|
Spawns a monster.
|
|
"""
|
|
@spec spawn_monster(t(), integer()) :: :ok
|
|
def spawn_monster(api, mob_id) do
|
|
spawn_monster_qty(api, mob_id, 1)
|
|
end
|
|
|
|
@doc """
|
|
Spawns multiple monsters.
|
|
"""
|
|
@spec spawn_monster_qty(t(), integer(), integer()) :: :ok
|
|
def spawn_monster_qty(api, mob_id, qty) do
|
|
Logger.debug("Spawn monster #{mob_id} x#{qty} for character #{api.character_id}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: map_id} ->
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
|
|
# Get player's position for spawn location
|
|
spawn_position = get_player_position(api)
|
|
|
|
Enum.each(1..qty, fn _ ->
|
|
spawn_monster_on_map(map_id, channel_id, mob_id, spawn_position)
|
|
end)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Spawns monster at position.
|
|
"""
|
|
@spec spawn_monster_pos(t(), integer(), integer(), integer(), integer()) :: :ok
|
|
def spawn_monster_pos(api, mob_id, qty, x, y) do
|
|
Logger.debug("Spawn monster #{mob_id} x#{qty} at (#{x}, #{y})")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: map_id} ->
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
position = %{x: x, y: y, fh: 0}
|
|
|
|
Enum.each(1..qty, fn _ ->
|
|
spawn_monster_on_map(map_id, channel_id, mob_id, position)
|
|
end)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Kills all monsters on current map.
|
|
"""
|
|
@spec kill_all_mob(t()) :: :ok
|
|
def kill_all_mob(api) do
|
|
Logger.debug("Kill all monsters for character #{api.character_id}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: map_id} ->
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
|
|
# Get all monsters on map
|
|
monsters = Map.get_monsters(map_id, channel_id)
|
|
|
|
Enum.each(monsters, fn {oid, monster} ->
|
|
# Kill monster
|
|
Map.monster_killed(map_id, channel_id, oid, api.character_id)
|
|
|
|
# Broadcast kill packet
|
|
kill_packet = Packets.kill_monster(monster, 1)
|
|
Map.broadcast(map_id, channel_id, kill_packet)
|
|
end)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Kills specific monster.
|
|
"""
|
|
@spec kill_mob(t(), integer()) :: :ok
|
|
def kill_mob(api, mob_id) do
|
|
Logger.debug("Kill monster #{mob_id} for character #{api.character_id}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: map_id} ->
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
|
|
# Find and kill monster by mob_id
|
|
monsters = Map.get_monsters(map_id, channel_id)
|
|
|
|
Enum.each(monsters, fn {oid, monster} ->
|
|
if monster.mob_id == mob_id do
|
|
Map.monster_killed(map_id, channel_id, oid, api.character_id)
|
|
kill_packet = Packets.kill_monster(monster, 1)
|
|
Map.broadcast(map_id, channel_id, kill_packet)
|
|
end
|
|
end)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Spawns an NPC.
|
|
"""
|
|
@spec spawn_npc(t(), integer()) :: :ok
|
|
def spawn_npc(api, npc_id) do
|
|
# TODO: Get player position and spawn
|
|
Logger.debug("Spawn NPC #{npc_id}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Spawns NPC at position.
|
|
"""
|
|
@spec spawn_npc_pos(t(), integer(), integer(), integer()) :: :ok
|
|
def spawn_npc_pos(_api, npc_id, x, y) do
|
|
Logger.debug("Spawn NPC #{npc_id} at (#{x}, #{y})")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Removes an NPC from current map.
|
|
"""
|
|
@spec remove_npc(t(), integer()) :: :ok
|
|
def remove_npc(_api, npc_id) do
|
|
Logger.debug("Remove NPC #{npc_id}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Resets a map.
|
|
"""
|
|
@spec reset_map(t(), integer()) :: :ok
|
|
def reset_map(_api, map_id) do
|
|
Logger.debug("Reset map #{map_id}")
|
|
# TODO: Reset map
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Party Operations
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Checks if player is party leader.
|
|
"""
|
|
@spec is_leader(t()) :: boolean()
|
|
def is_leader(_api) do
|
|
# TODO: Check party leadership
|
|
false
|
|
end
|
|
|
|
@doc """
|
|
Gets party members in current map.
|
|
"""
|
|
@spec party_members_in_map(t()) :: integer()
|
|
def party_members_in_map(_api) do
|
|
# TODO: Count party members in map
|
|
1
|
|
end
|
|
|
|
@doc """
|
|
Gets all party members.
|
|
"""
|
|
@spec get_party_members(t()) :: [term()]
|
|
def get_party_members(_api) do
|
|
[]
|
|
end
|
|
|
|
@doc """
|
|
Checks if all party members are in current map.
|
|
"""
|
|
@spec all_members_here(t()) :: boolean()
|
|
def all_members_here(_api) do
|
|
true
|
|
end
|
|
|
|
@doc """
|
|
Warps party with EXP reward.
|
|
"""
|
|
@spec warp_party_with_exp(t(), integer(), integer()) :: :ok
|
|
def warp_party_with_exp(api, map_id, exp) do
|
|
warp_party(api, map_id)
|
|
gain_exp(api, exp)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Warps party with EXP and meso reward.
|
|
"""
|
|
@spec warp_party_with_exp_meso(t(), integer(), integer(), integer()) :: :ok
|
|
def warp_party_with_exp_meso(api, map_id, exp, meso) do
|
|
warp_party(api, map_id)
|
|
gain_exp(api, exp)
|
|
gain_meso(api, meso)
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Guild Operations
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Gets guild ID.
|
|
"""
|
|
@spec get_guild_id(t()) :: integer()
|
|
def get_guild_id(_api) do
|
|
0
|
|
end
|
|
|
|
@doc """
|
|
Increases guild capacity.
|
|
"""
|
|
@spec increase_guild_capacity(t(), boolean()) :: boolean()
|
|
def increase_guild_capacity(_api, _true_max) do
|
|
false
|
|
end
|
|
|
|
@doc """
|
|
Displays guild ranks.
|
|
"""
|
|
@spec display_guild_ranks(t()) :: :ok
|
|
def display_guild_ranks(_api) do
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Messages
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Sends a message to the player.
|
|
"""
|
|
@spec player_message(t(), String.t()) :: :ok
|
|
def player_message(api, message) do
|
|
player_message_type(api, 5, message)
|
|
end
|
|
|
|
@doc """
|
|
Sends a message with type.
|
|
|
|
Types: 1 = Popup, 5 = Chat, -1 = Important
|
|
"""
|
|
@spec player_message_type(t(), integer(), String.t()) :: :ok
|
|
def player_message_type(api, type, message) do
|
|
Logger.debug("Player message (#{type}): #{message}")
|
|
|
|
packet = Packets.drop_message(type, message)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a message to the map.
|
|
"""
|
|
@spec map_message(t(), String.t()) :: :ok
|
|
def map_message(api, message) do
|
|
map_message_type(api, 5, message)
|
|
end
|
|
|
|
@doc """
|
|
Sends a message to the map with type.
|
|
"""
|
|
@spec map_message_type(t(), integer(), String.t()) :: :ok
|
|
def map_message_type(api, type, message) do
|
|
Logger.debug("Map message (#{type}): #{message}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{map_id: map_id} ->
|
|
{:ok, channel_id} = Character.get_channel(api.character_id)
|
|
|
|
packet = Packets.server_notice(type, message)
|
|
Map.broadcast(map_id, channel_id, packet)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a world message.
|
|
"""
|
|
@spec world_message(t(), integer(), String.t()) :: :ok
|
|
def world_message(_api, type, message) do
|
|
Logger.debug("World message (#{type}): #{message}")
|
|
|
|
# Broadcast to all channels
|
|
packet = Packets.server_notice(type, message)
|
|
Odinsea.World.broadcast(packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Sends a guild message.
|
|
"""
|
|
@spec guild_message(t(), String.t()) :: :ok
|
|
def guild_message(api, message) do
|
|
Logger.debug("Guild message: #{message}")
|
|
|
|
case Character.get_state(api.character_id) do
|
|
nil -> :ok
|
|
%Character.State{guild_id: guild_id} when guild_id > 0 ->
|
|
packet = Packets.server_notice(5, message)
|
|
Odinsea.World.Guild.guild_packet(guild_id, packet)
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Shows quest message.
|
|
"""
|
|
@spec show_quest_msg(t(), String.t()) :: :ok
|
|
def show_quest_msg(api, msg) do
|
|
Logger.debug("Quest message: #{msg}")
|
|
|
|
packet = show_quest_msg_packet(msg)
|
|
send_packet(api.client_pid, packet)
|
|
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Storage/Shop
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Opens storage.
|
|
"""
|
|
@spec open_storage(t()) :: :ok
|
|
def open_storage(_api) do
|
|
Logger.debug("Open storage")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Opens a shop.
|
|
"""
|
|
@spec open_shop(t(), integer()) :: :ok
|
|
def open_shop(_api, shop_id) do
|
|
Logger.debug("Open shop #{shop_id}")
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Event/Instance
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Gets event manager.
|
|
"""
|
|
@spec get_event_manager(t(), String.t()) :: term()
|
|
def get_event_manager(_api, _event_name) do
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Gets event instance.
|
|
"""
|
|
@spec get_event_instance(t()) :: term()
|
|
def get_event_instance(_api) do
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Removes player from instance.
|
|
"""
|
|
@spec remove_player_from_instance(t()) :: boolean()
|
|
def remove_player_from_instance(_api) do
|
|
false
|
|
end
|
|
|
|
@doc """
|
|
Checks if player is in an instance.
|
|
"""
|
|
@spec is_player_instance(t()) :: boolean()
|
|
def is_player_instance(_api) do
|
|
false
|
|
end
|
|
|
|
# ============================================================================
|
|
# Miscellaneous
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Opens an NPC by ID.
|
|
"""
|
|
@spec open_npc(t(), integer()) :: :ok
|
|
def open_npc(%__MODULE__{client_pid: cpid, character_id: cid}, npc_id) do
|
|
Odinsea.Scripting.NPCManager.start_conversation(cpid, cid, npc_id)
|
|
end
|
|
|
|
@doc """
|
|
Opens an NPC with specific script.
|
|
"""
|
|
@spec open_npc_script(t(), integer(), String.t()) :: :ok
|
|
def open_npc_script(%__MODULE__{client_pid: cpid, character_id: cid}, npc_id, script_name) do
|
|
Odinsea.Scripting.NPCManager.start_conversation(cpid, cid, npc_id, script_name: script_name)
|
|
end
|
|
|
|
@doc """
|
|
Gets player name.
|
|
"""
|
|
@spec get_name(t()) :: String.t()
|
|
def get_name(_api) do
|
|
"Unknown"
|
|
end
|
|
|
|
@doc """
|
|
Gets channel number.
|
|
"""
|
|
@spec get_channel(t()) :: integer()
|
|
def get_channel(_api) do
|
|
1
|
|
end
|
|
|
|
@doc """
|
|
Adds HP.
|
|
"""
|
|
@spec add_hp(t(), integer()) :: :ok
|
|
def add_hp(_api, amount) do
|
|
Logger.debug("Add HP: #{amount}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Shows an effect.
|
|
"""
|
|
@spec show_effect(t(), boolean(), String.t()) :: :ok
|
|
def show_effect(_api, _broadcast, effect) do
|
|
Logger.debug("Show effect: #{effect}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Plays a sound.
|
|
"""
|
|
@spec play_sound(t(), boolean(), String.t()) :: :ok
|
|
def play_sound(_api, _broadcast, sound) do
|
|
Logger.debug("Play sound: #{sound}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Changes background music.
|
|
"""
|
|
@spec change_music(t(), String.t()) :: :ok
|
|
def change_music(_api, song_name) do
|
|
Logger.debug("Change music: #{song_name}")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gains NX (cash points).
|
|
"""
|
|
@spec gain_nx(t(), integer()) :: :ok
|
|
def gain_nx(_api, amount) do
|
|
Logger.debug("Gain NX: #{amount}")
|
|
:ok
|
|
end
|
|
|
|
# ============================================================================
|
|
# Private Helper Functions
|
|
# ============================================================================
|
|
|
|
defp send_packet(client_pid, packet) when is_pid(client_pid) do
|
|
send(client_pid, {:send_packet, packet})
|
|
end
|
|
|
|
defp send_packet(_client_pid, _packet), do: :ok
|
|
|
|
# NPC Talk packet (LP_NPCTalk)
|
|
# Reference: MaplePacketCreator.getNPCTalk()
|
|
defp npc_talk_packet(npc_id, msg_type, text, style, speaker_type, speaker_id) do
|
|
packet = Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4) # NPC conversation type
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.encode_byte(msg_type)
|
|
|> Out.encode_string(text)
|
|
|
|
# Encode style bytes ("00 00", "00 01", etc.)
|
|
packet = encode_style(packet, style)
|
|
|
|
# Encode speaker type and id if not default
|
|
packet = if speaker_type != 0 do
|
|
packet
|
|
|> Out.encode_byte(speaker_type)
|
|
|> Out.encode_int(speaker_id)
|
|
else
|
|
packet
|
|
end
|
|
|
|
Out.to_data(packet)
|
|
end
|
|
|
|
defp encode_style(packet, ""), do: packet
|
|
defp encode_style(packet, style) when is_binary(style) do
|
|
# Parse style string like "00 01" into bytes
|
|
bytes = style
|
|
|> String.split()
|
|
|> Enum.map(fn hex ->
|
|
case Integer.parse(hex, 16) do
|
|
{n, _} -> n
|
|
:error -> 0
|
|
end
|
|
end)
|
|
|
|
Enum.reduce(bytes, packet, fn byte_val, p ->
|
|
Out.encode_byte(p, byte_val)
|
|
end)
|
|
end
|
|
|
|
# NPC Talk Text packet (text input)
|
|
defp npc_talk_text_packet(npc_id, text, min, max, default) do
|
|
Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4)
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.encode_byte(@msg_get_text)
|
|
|> Out.encode_string(text)
|
|
|> Out.encode_string(default)
|
|
|> Out.encode_short(min)
|
|
|> Out.encode_short(max)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# NPC Talk Num packet (number input)
|
|
defp npc_talk_num_packet(npc_id, text, default, min, max) do
|
|
Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4)
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.encode_byte(@msg_get_number)
|
|
|> Out.encode_string(text)
|
|
|> Out.encode_int(default)
|
|
|> Out.encode_int(min)
|
|
|> Out.encode_int(max)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# NPC Talk Style packet (style selection)
|
|
defp npc_talk_style_packet(npc_id, text, styles, _page) do
|
|
packet = Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4)
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.encode_byte(@msg_style)
|
|
|> Out.encode_string(text)
|
|
|> Out.encode_byte(length(styles))
|
|
|
|
# Encode style IDs
|
|
packet = Enum.reduce(styles, packet, fn style_id, p ->
|
|
Out.encode_int(p, style_id)
|
|
end)
|
|
|
|
Out.to_data(packet)
|
|
end
|
|
|
|
# Map Selection packet
|
|
defp map_selection_packet(npc_id, selection_string) do
|
|
Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4)
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.encode_byte(0x10) # Map selection type
|
|
|> Out.encode_string(selection_string)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Update Stats packet
|
|
defp update_stat_packet(stats, job) do
|
|
packet = Out.new(Opcodes.lp_update_stats())
|
|
|> Out.encode_byte(1) # Reset flag
|
|
|
|
# Calculate stat mask
|
|
mask = Enum.reduce(stats, 0, fn {stat, _}, acc ->
|
|
acc ||| stat_mask(stat)
|
|
end)
|
|
|
|
packet = if Odinsea.Constants.Game.gms?() do
|
|
Out.encode_long(packet, mask)
|
|
else
|
|
Out.encode_int(packet, mask)
|
|
end
|
|
|
|
# Encode stat values
|
|
packet = Enum.reduce(stats, packet, fn {stat, value}, p ->
|
|
encode_stat_value(p, stat, value)
|
|
end)
|
|
|
|
# Encode job
|
|
Out.encode_short(packet, job)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
defp stat_mask(:skin), do: 0x01
|
|
defp stat_mask(:face), do: 0x02
|
|
defp stat_mask(:hair), do: 0x04
|
|
defp stat_mask(:pet), do: 0x08
|
|
defp stat_mask(:level), do: 0x10
|
|
defp stat_mask(:job), do: 0x20
|
|
defp stat_mask(:str), do: 0x40
|
|
defp stat_mask(:dex), do: 0x80
|
|
defp stat_mask(:int), do: 0x100
|
|
defp stat_mask(:luk), do: 0x200
|
|
defp stat_mask(:hp), do: 0x400
|
|
defp stat_mask(:max_hp), do: 0x800
|
|
defp stat_mask(:mp), do: 0x1000
|
|
defp stat_mask(:max_mp), do: 0x2000
|
|
defp stat_mask(:ap), do: 0x4000
|
|
defp stat_mask(:sp), do: 0x8000
|
|
defp stat_mask(:exp), do: 0x10000
|
|
defp stat_mask(:fame), do: 0x20000
|
|
defp stat_mask(:meso), do: 0x40000
|
|
defp stat_mask(_), do: 0
|
|
|
|
defp encode_stat_value(packet, stat, value) when stat in [:skin, :level] do
|
|
Out.encode_byte(packet, value)
|
|
end
|
|
defp encode_stat_value(packet, _stat, value) do
|
|
Out.encode_int(packet, value)
|
|
end
|
|
|
|
# Update Skill packet
|
|
defp update_skill_packet(skill_id, level, master_level, expiration) do
|
|
Out.new(Opcodes.lp_change_skill_record_result())
|
|
|> Out.encode_byte(1) # Success
|
|
|> Out.encode_byte(1) # Count
|
|
|> Out.encode_int(skill_id)
|
|
|> Out.encode_int(level)
|
|
|> Out.encode_int(master_level)
|
|
|> Out.encode_long(expiration)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Quest Operation packet
|
|
defp quest_operation_packet(quest_id, status) do
|
|
# status: 0 = forfeit, 1 = start, 2 = complete
|
|
mode = case status do
|
|
0 -> 0 # Forfeit
|
|
1 -> 1 # Start
|
|
2 -> 2 # Complete
|
|
_ -> 1
|
|
end
|
|
|
|
Out.new(Opcodes.lp_quest_clear())
|
|
|> Out.encode_byte(mode)
|
|
|> Out.encode_int(quest_id)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Show Foreign Effect packet (for job change, etc.)
|
|
defp show_foreign_effect_packet(character_id, effect_type) do
|
|
Out.new(Opcodes.lp_show_foreign_effect())
|
|
|> Out.encode_int(character_id)
|
|
|> Out.encode_byte(effect_type)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Show Own Buff Effect packet
|
|
defp show_own_buff_effect_packet(skill_id, effect_type, direction, level) do
|
|
Out.new(Opcodes.lp_show_own_buff_effect())
|
|
|> Out.encode_int(skill_id)
|
|
|> Out.encode_byte(effect_type)
|
|
|> Out.encode_byte(direction)
|
|
|> Out.encode_byte(level)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Show Quest Message packet
|
|
defp show_quest_msg_packet(msg) do
|
|
Out.new(Opcodes.lp_quest_clear())
|
|
|> Out.encode_byte(0x0B) # Show quest message mode
|
|
|> Out.encode_string(msg)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# Simple selection helper for speaker dialogs
|
|
defp send_simple_s_npc(api, text, speaker_type, npc_id) do
|
|
packet = Out.new(Opcodes.lp_npc_talk())
|
|
|> Out.encode_byte(4)
|
|
|> Out.encode_int(api.npc_id)
|
|
|> Out.encode_byte(5) # Simple type
|
|
|> Out.encode_string(text)
|
|
|> Out.encode_byte(speaker_type)
|
|
|> Out.encode_int(npc_id)
|
|
|> Out.to_data()
|
|
|
|
send_packet(api.client_pid, packet)
|
|
set_last_msg(api, @msg_simple)
|
|
end
|
|
|
|
# Spawn monster on map helper
|
|
defp spawn_monster_on_map(map_id, channel_id, mob_id, position) do
|
|
# Get monster stats from LifeFactory
|
|
case LifeFactory.get_monster_stats(mob_id) do
|
|
nil ->
|
|
Logger.warn("Monster stats not found for mob_id #{mob_id}")
|
|
:error
|
|
|
|
stats ->
|
|
# Create monster
|
|
monster = %Monster{
|
|
oid: 0, # Will be assigned by map
|
|
mob_id: mob_id,
|
|
stats: stats,
|
|
hp: stats.hp,
|
|
mp: stats.mp,
|
|
max_hp: stats.hp,
|
|
max_mp: stats.mp,
|
|
position: position,
|
|
stance: 5,
|
|
controller_id: nil,
|
|
controller_has_aggro: false,
|
|
spawn_effect: 0,
|
|
team: -1,
|
|
fake: false,
|
|
link_oid: 0,
|
|
status_effects: %{},
|
|
poisons: [],
|
|
attackers: %{},
|
|
last_attack: System.system_time(:millisecond),
|
|
last_move: System.system_time(:millisecond),
|
|
last_skill_use: 0,
|
|
killed: false,
|
|
drops_disabled: false,
|
|
create_time: System.system_time(:millisecond)
|
|
}
|
|
|
|
# Spawn on map - the map will assign OID and broadcast
|
|
# This is a simplified version - full implementation needs Map.spawn_monster_at
|
|
spawn_packet = Packets.spawn_monster(monster, -1, 0)
|
|
Map.broadcast(map_id, channel_id, spawn_packet)
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# Get player position helper
|
|
defp get_player_position(api) do
|
|
case Character.get_state(api.character_id) do
|
|
nil -> %{x: 0, y: 0, fh: 0}
|
|
%Character.State{position: pos} -> %{x: pos.x, y: pos.y, fh: pos.foothold}
|
|
end
|
|
end
|
|
end
|