346 lines
9.6 KiB
Elixir
346 lines
9.6 KiB
Elixir
defmodule Odinsea.Scripting.PortalManager do
|
|
@moduledoc """
|
|
Portal Script Manager for handling scripted portals.
|
|
|
|
Portal scripts are triggered when a player enters a portal with a script name.
|
|
They receive a `pi` (portal interaction) API object that extends PlayerAPI
|
|
with portal-specific functionality.
|
|
|
|
## Script Interface
|
|
|
|
Portal scripts must implement the `enter/1` callback:
|
|
|
|
defmodule Odinsea.Scripting.Portal.Script_08_xmas_st do
|
|
@behaviour Odinsea.Scripting.Behavior
|
|
|
|
alias Odinsea.Scripting.PlayerAPI
|
|
|
|
@impl true
|
|
def enter(pi) do
|
|
# Portal logic here
|
|
if PlayerAPI.get_player_stat(pi, "LVL") >= 10 do
|
|
PlayerAPI.warp(pi, 100000000)
|
|
:ok
|
|
else
|
|
PlayerAPI.player_message(pi, "You must be level 10 to enter.")
|
|
{:error, :level_too_low}
|
|
end
|
|
end
|
|
end
|
|
|
|
## JavaScript Compatibility
|
|
|
|
For JavaScript scripts:
|
|
- `pi` - Portal interaction API
|
|
- `function enter(pi)` - Entry point
|
|
|
|
## Portal API Extensions
|
|
|
|
The portal API (`pi`) includes all PlayerAPI functions plus:
|
|
- `get_portal/0` - Get portal data
|
|
- `in_free_market/0` - Warp to free market
|
|
- `in_ardentmill/0` - Warp to crafting town
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
alias Odinsea.Scripting.{Manager, PlayerAPI}
|
|
|
|
# ETS table for caching compiled portal scripts
|
|
@portal_cache :portal_scripts
|
|
|
|
# ============================================================================
|
|
# Types
|
|
# ============================================================================
|
|
|
|
@type portal_script :: module()
|
|
@type portal_result :: :ok | {:error, term()}
|
|
|
|
# ============================================================================
|
|
# Client API
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Starts the portal script manager.
|
|
"""
|
|
@spec start_link(keyword()) :: GenServer.on_start()
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Executes a portal script when a player enters a scripted portal.
|
|
|
|
## Parameters
|
|
- `script_name` - Name of the portal script (e.g., "08_xmas_st")
|
|
- `client_pid` - Player's client process
|
|
- `character_id` - Character ID
|
|
- `portal_data` - Portal information (position, target map, etc.)
|
|
|
|
## Returns
|
|
- `:ok` - Script executed successfully
|
|
- `{:error, reason}` - Script execution failed or script not found
|
|
"""
|
|
@spec execute(String.t(), pid(), integer(), map()) :: portal_result()
|
|
def execute(script_name, client_pid, character_id, portal_data) do
|
|
GenServer.call(__MODULE__, {
|
|
:execute,
|
|
script_name,
|
|
client_pid,
|
|
character_id,
|
|
portal_data
|
|
})
|
|
end
|
|
|
|
@doc """
|
|
Loads a portal script into the cache.
|
|
|
|
## Parameters
|
|
- `script_name` - Name of the script
|
|
|
|
## Returns
|
|
- `{:ok, module}` - Script loaded
|
|
- `{:error, reason}` - Failed to load
|
|
"""
|
|
@spec load_script(String.t()) :: {:ok, module()} | {:error, term()}
|
|
def load_script(script_name) do
|
|
GenServer.call(__MODULE__, {:load_script, script_name})
|
|
end
|
|
|
|
@doc """
|
|
Gets a cached portal script.
|
|
|
|
## Parameters
|
|
- `script_name` - Name of the script
|
|
|
|
## Returns
|
|
- `{:ok, module}` - Script found
|
|
- `{:error, :not_found}` - Script not cached
|
|
"""
|
|
@spec get_script(String.t()) :: {:ok, module()} | {:error, term()}
|
|
def get_script(script_name) do
|
|
case :ets.lookup(@portal_cache, script_name) do
|
|
[{^script_name, module}] -> {:ok, module}
|
|
[] -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Clears all cached portal scripts.
|
|
"""
|
|
@spec clear_cache() :: :ok
|
|
def clear_cache() do
|
|
GenServer.call(__MODULE__, :clear_cache)
|
|
end
|
|
|
|
@doc """
|
|
Lists all available portal scripts.
|
|
|
|
## Returns
|
|
- List of script names
|
|
"""
|
|
@spec list_scripts() :: [String.t()]
|
|
def list_scripts() do
|
|
Manager.list_scripts(:portal)
|
|
end
|
|
|
|
@doc """
|
|
Checks if a portal script exists.
|
|
|
|
## Parameters
|
|
- `script_name` - Name of the script
|
|
|
|
## Returns
|
|
- `true` - Script exists
|
|
- `false` - Script does not exist
|
|
"""
|
|
@spec script_exists?(String.t()) :: boolean()
|
|
def script_exists?(script_name) do
|
|
Manager.script_exists?(:portal, script_name)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Server Callbacks
|
|
# ============================================================================
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Create ETS table for caching portal scripts
|
|
:ets.new(@portal_cache, [:named_table, :set, :public,
|
|
read_concurrency: true, write_concurrency: true])
|
|
|
|
Logger.info("Portal Script Manager initialized")
|
|
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:execute, script_name, client_pid, character_id, portal_data}, _from, state) do
|
|
# Get or load the script
|
|
script_result = case get_script(script_name) do
|
|
{:ok, module} -> {:ok, module}
|
|
{:error, :not_found} -> do_load_script(script_name)
|
|
end
|
|
|
|
case script_result do
|
|
{:ok, script_module} ->
|
|
# Create portal interaction API
|
|
pi = create_portal_api(client_pid, character_id, portal_data)
|
|
|
|
# Execute the script's enter function
|
|
result = try do
|
|
if function_exported?(script_module, :enter, 1) do
|
|
script_module.enter(pi)
|
|
else
|
|
Logger.warning("Portal script #{script_name} missing enter/1 function")
|
|
{:error, :invalid_script}
|
|
end
|
|
rescue
|
|
e ->
|
|
Logger.error("Portal script #{script_name} error: #{inspect(e)}")
|
|
{:error, :script_error}
|
|
catch
|
|
kind, reason ->
|
|
Logger.error("Portal script #{script_name} crashed: #{kind} #{inspect(reason)}")
|
|
{:error, :script_crash}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Unhandled portal script #{script_name}: #{inspect(reason)}")
|
|
{:reply, {:error, reason}, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:load_script, script_name}, _from, state) do
|
|
result = do_load_script(script_name)
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:clear_cache, _from, state) do
|
|
:ets.delete_all_objects(@portal_cache)
|
|
Logger.info("Portal script cache cleared")
|
|
{:reply, :ok, state}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Private Functions
|
|
# ============================================================================
|
|
|
|
defp do_load_script(script_name) do
|
|
case Manager.get_script(:portal, script_name) do
|
|
{:ok, module} ->
|
|
:ets.insert(@portal_cache, {script_name, module})
|
|
{:ok, module}
|
|
|
|
{:error, reason} = error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
defp create_portal_api(client_pid, character_id, portal_data) do
|
|
# Create extended PlayerAPI with portal-specific functions
|
|
base_api = PlayerAPI.new(client_pid, character_id, portal_data.id, nil, nil)
|
|
|
|
# Add portal-specific data
|
|
Map.put(base_api, :__portal_data__, portal_data)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Portal API Extensions (for use in scripts)
|
|
# ============================================================================
|
|
|
|
defmodule PortalAPI do
|
|
@moduledoc """
|
|
Portal-specific API extensions.
|
|
|
|
These functions are available on the `pi` object passed to portal scripts.
|
|
"""
|
|
|
|
alias Odinsea.Scripting.PlayerAPI
|
|
|
|
@doc """
|
|
Gets the portal data.
|
|
|
|
## Parameters
|
|
- `pi` - Portal API struct
|
|
|
|
## Returns
|
|
- Portal data map
|
|
"""
|
|
@spec get_portal(PlayerAPI.t()) :: map()
|
|
def get_portal(%{__portal_data__: data}), do: data
|
|
def get_portal(_), do: %{}
|
|
|
|
@doc """
|
|
Gets portal position.
|
|
"""
|
|
@spec get_position(PlayerAPI.t()) :: {integer(), integer()}
|
|
def get_position(pi) do
|
|
case get_portal(pi) do
|
|
%{x: x, y: y} -> {x, y}
|
|
_ -> {0, 0}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Warps to Free Market if level >= 15.
|
|
"""
|
|
@spec in_free_market(PlayerAPI.t()) :: :ok
|
|
def in_free_market(pi) do
|
|
level = PlayerAPI.get_player_stat(pi, "LVL")
|
|
|
|
if level >= 15 do
|
|
# Save return location
|
|
PlayerAPI.save_location(pi, "FREE_MARKET")
|
|
PlayerAPI.play_portal_se(pi)
|
|
PlayerAPI.warp_portal(pi, 910000000, "st00")
|
|
else
|
|
PlayerAPI.player_message_type(pi, 5, "You must be level 15 to enter the Free Market.")
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Warps to Ardentmill (crafting town) if level >= 10.
|
|
"""
|
|
@spec in_ardentmill(PlayerAPI.t()) :: :ok
|
|
def in_ardentmill(pi) do
|
|
level = PlayerAPI.get_player_stat(pi, "LVL")
|
|
|
|
if level >= 10 do
|
|
PlayerAPI.save_location(pi, "ARDENTMILL")
|
|
PlayerAPI.play_portal_se(pi)
|
|
PlayerAPI.warp_portal(pi, 910001000, "st00")
|
|
else
|
|
PlayerAPI.player_message_type(pi, 5, "You must be level 10 to enter the Crafting Town.")
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Spawns monster at portal position.
|
|
"""
|
|
@spec spawn_monster(PlayerAPI.t(), integer()) :: :ok
|
|
def spawn_monster(pi, mob_id) do
|
|
{x, y} = get_position(pi)
|
|
PlayerAPI.spawn_monster_pos(pi, mob_id, 1, x, y)
|
|
end
|
|
|
|
@doc """
|
|
Spawns multiple monsters at portal position.
|
|
"""
|
|
@spec spawn_monsters(PlayerAPI.t(), integer(), integer()) :: :ok
|
|
def spawn_monsters(pi, mob_id, qty) do
|
|
{x, y} = get_position(pi)
|
|
PlayerAPI.spawn_monster_pos(pi, mob_id, qty, x, y)
|
|
end
|
|
end
|
|
end
|