kimi gone wild
This commit is contained in:
499
lib/odinsea/scripting/reactor_manager.ex
Normal file
499
lib/odinsea/scripting/reactor_manager.ex
Normal file
@@ -0,0 +1,499 @@
|
||||
defmodule Odinsea.Scripting.ReactorManager do
|
||||
@moduledoc """
|
||||
Reactor Script Manager for handling reactor (map object) interactions.
|
||||
|
||||
Reactor scripts are triggered when a player hits/activates a reactor.
|
||||
They receive an `rm` (reactor manager) API object that extends PlayerAPI
|
||||
with reactor-specific functionality like dropping items.
|
||||
|
||||
## Script Interface
|
||||
|
||||
Reactor scripts must implement the `act/1` callback:
|
||||
|
||||
defmodule Odinsea.Scripting.Reactor.Script_1002001 do
|
||||
@behaviour Odinsea.Scripting.Behavior
|
||||
|
||||
alias Odinsea.Scripting.PlayerAPI
|
||||
alias Odinsea.Scripting.ReactorManager.ReactorAPI
|
||||
|
||||
@impl true
|
||||
def act(rm) do
|
||||
# Drop items at reactor position
|
||||
ReactorAPI.drop_items(rm, true, 1, 100, 500)
|
||||
|
||||
# Or drop a single item
|
||||
ReactorAPI.drop_single_item(rm, 4000000)
|
||||
end
|
||||
end
|
||||
|
||||
## JavaScript Compatibility
|
||||
|
||||
For JavaScript scripts:
|
||||
- `rm` - Reactor action manager API
|
||||
- `function act()` - Entry point
|
||||
|
||||
## Reactor API Extensions
|
||||
|
||||
The reactor API (`rm`) includes all PlayerAPI functions plus:
|
||||
- `drop_items/5` - Drop items/meso at reactor position
|
||||
- `drop_single_item/2` - Drop a single item
|
||||
- `get_position/1` - Get reactor position
|
||||
- `spawn_zakum/1` - Spawn Zakum boss
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Scripting.{Manager, PlayerAPI}
|
||||
|
||||
# ETS table for caching reactor scripts
|
||||
@reactor_cache :reactor_scripts
|
||||
|
||||
# ETS table for reactor drops
|
||||
@reactor_drops :reactor_drops
|
||||
|
||||
# ============================================================================
|
||||
# Types
|
||||
# ============================================================================
|
||||
|
||||
@type reactor_script :: module()
|
||||
@type reactor_result :: :ok | {:error, term()}
|
||||
|
||||
defmodule DropEntry do
|
||||
@moduledoc "Represents a reactor drop entry."
|
||||
|
||||
defstruct [
|
||||
:item_id, # Item ID (0 = meso)
|
||||
:chance, # Drop chance (1 in N)
|
||||
:quest_id # Required quest (-1 = none)
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
item_id: integer(),
|
||||
chance: integer(),
|
||||
quest_id: integer()
|
||||
}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Client API
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Starts the reactor 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 reactor script when a player activates a reactor.
|
||||
|
||||
## Parameters
|
||||
- `reactor_id` - Reactor template ID
|
||||
- `client_pid` - Player's client process
|
||||
- `character_id` - Character ID
|
||||
- `reactor_instance` - Reactor instance data
|
||||
|
||||
## Returns
|
||||
- `:ok` - Script executed successfully
|
||||
- `{:error, reason}` - Script execution failed
|
||||
"""
|
||||
@spec act(integer(), pid(), integer(), map()) :: reactor_result()
|
||||
def act(reactor_id, client_pid, character_id, reactor_instance) do
|
||||
GenServer.call(__MODULE__, {
|
||||
:act,
|
||||
reactor_id,
|
||||
client_pid,
|
||||
character_id,
|
||||
reactor_instance
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets drops for a reactor.
|
||||
|
||||
## Parameters
|
||||
- `reactor_id` - Reactor template ID
|
||||
|
||||
## Returns
|
||||
- List of DropEntry structs
|
||||
"""
|
||||
@spec get_drops(integer()) :: [DropEntry.t()]
|
||||
def get_drops(reactor_id) do
|
||||
case :ets.lookup(@reactor_drops, reactor_id) do
|
||||
[{^reactor_id, drops}] -> drops
|
||||
[] -> load_drops(reactor_id)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Clears all cached reactor drops.
|
||||
"""
|
||||
@spec clear_drops() :: :ok
|
||||
def clear_drops() do
|
||||
GenServer.call(__MODULE__, :clear_drops)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads a reactor script into the cache.
|
||||
"""
|
||||
@spec load_script(integer()) :: {:ok, module()} | {:error, term()}
|
||||
def load_script(reactor_id) do
|
||||
GenServer.call(__MODULE__, {:load_script, reactor_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all available reactor scripts.
|
||||
"""
|
||||
@spec list_scripts() :: [String.t()]
|
||||
def list_scripts() do
|
||||
Manager.list_scripts(:reactor)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Server Callbacks
|
||||
# ============================================================================
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
# Create ETS tables
|
||||
:ets.new(@reactor_cache, [:named_table, :set, :public,
|
||||
read_concurrency: true, write_concurrency: true])
|
||||
:ets.new(@reactor_drops, [:named_table, :set, :public,
|
||||
read_concurrency: true, write_concurrency: true])
|
||||
|
||||
Logger.info("Reactor Script Manager initialized")
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:act, reactor_id, client_pid, character_id, reactor_instance}, _from, state) do
|
||||
# Get or load the script
|
||||
script_name = to_string(reactor_id)
|
||||
|
||||
script_result = case :ets.lookup(@reactor_cache, reactor_id) do
|
||||
[{^reactor_id, module}] -> {:ok, module}
|
||||
[] -> do_load_script(reactor_id)
|
||||
end
|
||||
|
||||
case script_result do
|
||||
{:ok, script_module} ->
|
||||
# Create reactor action API
|
||||
rm = create_reactor_api(client_pid, character_id, reactor_id, reactor_instance)
|
||||
|
||||
# Execute the script's act function
|
||||
result = try do
|
||||
if function_exported?(script_module, :act, 1) do
|
||||
script_module.act(rm)
|
||||
else
|
||||
Logger.warning("Reactor script #{reactor_id} missing act/1 function")
|
||||
# Execute default drop behavior
|
||||
ReactorAPI.drop_items(rm, false, 0, 0, 0)
|
||||
:ok
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Reactor script #{reactor_id} error: #{inspect(e)}")
|
||||
:ok # Don't error on reactor scripts, just log
|
||||
catch
|
||||
kind, reason ->
|
||||
Logger.error("Reactor script #{reactor_id} crashed: #{kind} #{inspect(reason)}")
|
||||
:ok
|
||||
end
|
||||
|
||||
{:reply, result, state}
|
||||
|
||||
{:error, _reason} ->
|
||||
# No script found - execute default drop behavior
|
||||
rm = create_reactor_api(client_pid, character_id, reactor_id, reactor_instance)
|
||||
ReactorAPI.drop_items(rm, false, 0, 0, 0)
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:load_script, reactor_id}, _from, state) do
|
||||
result = do_load_script(reactor_id)
|
||||
{:reply, result, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:clear_drops, _from, state) do
|
||||
:ets.delete_all_objects(@reactor_drops)
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Functions
|
||||
# ============================================================================
|
||||
|
||||
defp do_load_script(reactor_id) do
|
||||
script_name = to_string(reactor_id)
|
||||
|
||||
case Manager.get_script(:reactor, script_name) do
|
||||
{:ok, module} ->
|
||||
:ets.insert(@reactor_cache, {reactor_id, module})
|
||||
{:ok, module}
|
||||
|
||||
{:error, reason} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp load_drops(reactor_id) do
|
||||
# TODO: Load from database
|
||||
# For now, return empty list
|
||||
drops = []
|
||||
:ets.insert(@reactor_drops, {reactor_id, drops})
|
||||
drops
|
||||
end
|
||||
|
||||
defp create_reactor_api(client_pid, character_id, reactor_id, reactor_instance) do
|
||||
base_api = PlayerAPI.new(client_pid, character_id, reactor_id, nil, nil)
|
||||
|
||||
Map.merge(base_api, %{
|
||||
__reactor_instance__: reactor_instance,
|
||||
__reactor_id__: reactor_id
|
||||
})
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Reactor API Extensions (for use in scripts)
|
||||
# ============================================================================
|
||||
|
||||
defmodule ReactorAPI do
|
||||
@moduledoc """
|
||||
Reactor-specific API extensions.
|
||||
|
||||
These functions are available on the `rm` object passed to reactor scripts.
|
||||
"""
|
||||
|
||||
alias Odinsea.Scripting.PlayerAPI
|
||||
alias Odinsea.Scripting.ReactorManager.DropEntry
|
||||
|
||||
@doc """
|
||||
Gets the reactor instance data.
|
||||
"""
|
||||
@spec get_reactor(PlayerAPI.t()) :: map()
|
||||
def get_reactor(%{__reactor_instance__: data}), do: data
|
||||
def get_reactor(_), do: %{}
|
||||
|
||||
@doc """
|
||||
Gets reactor position.
|
||||
"""
|
||||
@spec get_position(PlayerAPI.t()) :: {integer(), integer()}
|
||||
def get_position(rm) do
|
||||
case get_reactor(rm) do
|
||||
%{x: x, y: y} -> {x, y - 10} # Slightly above for drops
|
||||
_ -> {0, 0}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets reactor ID.
|
||||
"""
|
||||
@spec get_reactor_id(PlayerAPI.t()) :: integer()
|
||||
def get_reactor_id(%{__reactor_id__: id}), do: id
|
||||
def get_reactor_id(_), do: 0
|
||||
|
||||
@doc """
|
||||
Drops items from reactor.
|
||||
|
||||
## Parameters
|
||||
- `rm` - Reactor API
|
||||
- `meso` - Whether to drop meso
|
||||
- `meso_chance` - Chance for meso (1 in N)
|
||||
- `min_meso` - Minimum meso amount
|
||||
- `max_meso` - Maximum meso amount
|
||||
- `min_items` - Minimum items to drop
|
||||
"""
|
||||
@spec drop_items(PlayerAPI.t(), boolean(), integer(), integer(), integer(), integer()) :: :ok
|
||||
def drop_items(rm, meso \\ false, meso_chance \\ 0, min_meso \\ 0, max_meso \\ 0, min_items \\ 0) do
|
||||
reactor_id = get_reactor_id(rm)
|
||||
chances = Odinsea.Scripting.ReactorManager.get_drops(reactor_id)
|
||||
|
||||
# Filter drops by chance
|
||||
items = filter_drops(chances, rm)
|
||||
|
||||
# Add meso if enabled
|
||||
items = if meso && :rand.uniform(meso_chance) == 1 do
|
||||
[%DropEntry{item_id: 0, chance: meso_chance, quest_id: -1} | items]
|
||||
else
|
||||
items
|
||||
end
|
||||
|
||||
# Pad with meso if needed
|
||||
items = if length(items) < min_items do
|
||||
pad_items(items, min_items, meso_chance)
|
||||
else
|
||||
items
|
||||
end
|
||||
|
||||
# Calculate drop position
|
||||
{base_x, y} = get_position(rm)
|
||||
count = length(items)
|
||||
start_x = base_x - (12 * count)
|
||||
|
||||
# Drop items
|
||||
Enum.each(Enum.with_index(items), fn {drop, idx} ->
|
||||
x = start_x + (idx * 25)
|
||||
|
||||
if drop.item_id == 0 do
|
||||
# Meso drop
|
||||
amount = :rand.uniform(max_meso - min_meso) + min_meso
|
||||
drop_meso(rm, amount, {x, y})
|
||||
else
|
||||
# Item drop
|
||||
drop_item(rm, drop.item_id, {x, y}, drop.quest_id)
|
||||
end
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Drops a single item at reactor position.
|
||||
"""
|
||||
@spec drop_single_item(PlayerAPI.t(), integer()) :: :ok
|
||||
def drop_single_item(rm, item_id) do
|
||||
pos = get_position(rm)
|
||||
drop_item(rm, item_id, pos, -1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns Zakum at reactor position.
|
||||
"""
|
||||
@spec spawn_zakum(PlayerAPI.t()) :: :ok
|
||||
def spawn_zakum(rm) do
|
||||
{x, y} = get_position(rm)
|
||||
Logger.debug("Spawn Zakum at (#{x}, #{y})")
|
||||
# TODO: Spawn Zakum
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns a fake (non-aggro) monster at reactor position.
|
||||
"""
|
||||
@spec spawn_fake_monster(PlayerAPI.t(), integer()) :: :ok
|
||||
def spawn_fake_monster(rm, mob_id) do
|
||||
spawn_fake_monster_qty(rm, mob_id, 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns multiple fake monsters at reactor position.
|
||||
"""
|
||||
@spec spawn_fake_monster_qty(PlayerAPI.t(), integer(), integer()) :: :ok
|
||||
def spawn_fake_monster_qty(rm, mob_id, qty) do
|
||||
{x, y} = get_position(rm)
|
||||
Logger.debug("Spawn fake monster #{mob_id} x#{qty} at (#{x}, #{y})")
|
||||
# TODO: Spawn fake monsters
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns NPC at reactor position.
|
||||
"""
|
||||
@spec spawn_npc(PlayerAPI.t(), integer()) :: :ok
|
||||
def spawn_npc(rm, npc_id) do
|
||||
{x, y} = get_position(rm)
|
||||
PlayerAPI.spawn_npc_pos(rm, npc_id, x, y)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kills all monsters on the map.
|
||||
"""
|
||||
@spec kill_all(PlayerAPI.t()) :: :ok
|
||||
def kill_all(rm) do
|
||||
PlayerAPI.kill_all_mob(rm)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kills a specific monster.
|
||||
"""
|
||||
@spec kill_monster(PlayerAPI.t(), integer()) :: :ok
|
||||
def kill_monster(rm, mob_id) do
|
||||
PlayerAPI.kill_mob(rm, mob_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispels all monsters (CPQ guardian effect).
|
||||
"""
|
||||
@spec dispel_all_monsters(PlayerAPI.t(), integer()) :: :ok
|
||||
def dispel_all_monsters(rm, _num) do
|
||||
# TODO: Dispel monsters
|
||||
Logger.debug("Dispel all monsters")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs harvesting (profession gathering).
|
||||
"""
|
||||
@spec do_harvest(PlayerAPI.t()) :: :ok
|
||||
def do_harvest(rm) do
|
||||
# TODO: Implement harvesting logic
|
||||
Logger.debug("Harvesting at reactor")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancels harvesting.
|
||||
"""
|
||||
@spec cancel_harvest(PlayerAPI.t(), boolean()) :: :ok
|
||||
def cancel_harvest(_rm, _success) do
|
||||
:ok
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
defp filter_drops(chances, rm) do
|
||||
Enum.filter(chances, fn drop ->
|
||||
passed_chance = :rand.uniform(drop.chance) == 1
|
||||
passed_quest = should_drop_quest_item(drop.quest_id, rm)
|
||||
passed_chance && passed_quest
|
||||
end)
|
||||
end
|
||||
|
||||
defp should_drop_quest_item(quest_id, _rm) when quest_id <= 0, do: true
|
||||
defp should_drop_quest_item(quest_id, rm) do
|
||||
# TODO: Check if any player on map has quest active
|
||||
# For now, return true
|
||||
true
|
||||
end
|
||||
|
||||
defp pad_items(items, min_items, meso_chance) when length(items) < min_items do
|
||||
pad_items(
|
||||
[%DropEntry{item_id: 0, chance: meso_chance, quest_id: -1} | items],
|
||||
min_items,
|
||||
meso_chance
|
||||
)
|
||||
end
|
||||
defp pad_items(items, _min, _chance), do: items
|
||||
|
||||
defp drop_meso(rm, amount, position) do
|
||||
Logger.debug("Drop #{amount} meso at #{inspect(position)}")
|
||||
# TODO: Spawn meso drop
|
||||
:ok
|
||||
end
|
||||
|
||||
defp drop_item(rm, item_id, position, quest_id) do
|
||||
owner = get_drop_owner(quest_id, rm)
|
||||
Logger.debug("Drop item #{item_id} at #{inspect(position)}, owner: #{inspect(owner)}")
|
||||
# TODO: Spawn item drop
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_drop_owner(quest_id, rm) when quest_id <= 0 do
|
||||
# Return triggering player
|
||||
rm.character_id
|
||||
end
|
||||
defp get_drop_owner(_quest_id, rm) do
|
||||
# TODO: Find player who needs quest item
|
||||
rm.character_id
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user