defmodule Odinsea.Game.ReactorStats do @moduledoc """ Represents reactor template stats (state machine data). Contains the state definitions for a reactor type. Each state defines: type, next state, required item, timeout, touch mode. Ported from Java: src/server/maps/MapleReactorStats.java """ defmodule Point do @moduledoc "Simple 2D point for area bounds" @type t :: %__MODULE__{x: integer(), y: integer()} defstruct [:x, :y] end defmodule StateData do @moduledoc "State definition for a reactor" @type t :: %__MODULE__{ type: integer(), # State type (determines behavior) next_state: integer(), # Next state index (-1 = end) react_item: {integer(), integer()} | nil, # {item_id, quantity} required timeout: integer(), # Timeout in ms before auto-advance (-1 = none) can_touch: integer() # 0 = hit only, 1 = click/touch, 2 = touch only } defstruct [ :type, :next_state, :react_item, timeout: -1, can_touch: 0 ] end @typedoc "Reactor stats struct" @type t :: %__MODULE__{ tl: Point.t() | nil, # Top-left corner of area (for item-triggered) br: Point.t() | nil, # Bottom-right corner of area states: %{integer() => StateData.t()}, # State definitions by state number activate_by_touch: boolean() # Whether reactor activates by touch } defstruct [ :tl, :br, states: %{}, activate_by_touch: false ] @doc """ Creates a new empty reactor stats. """ @spec new() :: t() def new do %__MODULE__{} end @doc """ Sets the top-left point of the area. """ @spec set_tl(t(), integer(), integer()) :: t() def set_tl(stats, x, y) do %{stats | tl: %Point{x: x, y: y}} end @doc """ Sets the bottom-right point of the area. """ @spec set_br(t(), integer(), integer()) :: t() def set_br(stats, x, y) do %{stats | br: %Point{x: x, y: y}} end @doc """ Sets whether reactor activates by touch. """ @spec set_activate_by_touch(t(), boolean()) :: t() def set_activate_by_touch(stats, activate) do %{stats | activate_by_touch: activate} end @doc """ Adds a state definition. ## Parameters - stats: the reactor stats struct - state_num: the state number (byte value) - type: the state type (determines behavior) - react_item: {item_id, quantity} or nil - next_state: the next state number (-1 for end) - timeout: timeout in ms (-1 for none) - can_touch: 0 = hit only, 1 = click, 2 = touch only """ @spec add_state( t(), integer(), integer(), {integer(), integer()} | nil, integer(), integer(), integer() ) :: t() def add_state(stats, state_num, type, react_item, next_state, timeout, can_touch) do state_data = %StateData{ type: type, react_item: react_item, next_state: next_state, timeout: timeout, can_touch: can_touch } %{stats | states: Map.put(stats.states, state_num, state_data)} end @doc """ Gets the next state for a given current state. Returns -1 if not found. """ @spec get_next_state(t(), integer()) :: integer() def get_next_state(stats, state) do case Map.get(stats.states, state) do nil -> -1 state_data -> state_data.next_state end end @doc """ Gets the type for a given state. Returns -1 if not found. """ @spec get_type(t(), integer()) :: integer() def get_type(stats, state) do case Map.get(stats.states, state) do nil -> -1 state_data -> state_data.type end end @doc """ Gets the react item for a given state. Returns nil if not found. """ @spec get_react_item(t(), integer()) :: {integer(), integer()} | nil def get_react_item(stats, state) do case Map.get(stats.states, state) do nil -> nil state_data -> state_data.react_item end end @doc """ Gets the timeout for a given state. Returns -1 if not found. """ @spec get_timeout(t(), integer()) :: integer() def get_timeout(stats, state) do case Map.get(stats.states, state) do nil -> -1 state_data -> state_data.timeout end end @doc """ Gets the touch mode for a given state. Returns 0 if not found. Modes: - 0: Hit only (weapon attack) - 1: Click/touch (interact button) - 2: Touch only (walk into) """ @spec can_touch(t(), integer()) :: integer() def can_touch(stats, state) do case Map.get(stats.states, state) do nil -> 0 state_data -> state_data.can_touch end end @doc """ Gets all state numbers defined for this reactor. """ @spec get_state_numbers(t()) :: [integer()] def get_state_numbers(stats) do Map.keys(stats.states) |> Enum.sort() end @doc """ Checks if a state exists. """ @spec has_state?(t(), integer()) :: boolean() def has_state?(stats, state) do Map.has_key?(stats.states, state) end @doc """ Gets the state data for a given state number. """ @spec get_state_data(t(), integer()) :: StateData.t() | nil def get_state_data(stats, state) do Map.get(stats.states, state) end @doc """ Builds reactor stats from JSON data. """ @spec from_json(map()) :: t() def from_json(data) do stats = new() # Set activate by touch stats = set_activate_by_touch(stats, data["activate_by_touch"] == true) # Set area bounds if present stats = if data["tl"] do set_tl(stats, data["tl"]["x"] || 0, data["tl"]["y"] || 0) else stats end stats = if data["br"] do set_br(stats, data["br"]["x"] || 0, data["br"]["y"] || 0) else stats end # Add states states = data["states"] || %{} Enum.reduce(states, stats, fn {state_num_str, state_data}, acc_stats -> state_num = String.to_integer(state_num_str) type = state_data["type"] || 999 next_state = state_data["next_state"] || -1 timeout = state_data["timeout"] || -1 can_touch = state_data["can_touch"] || 0 react_item = if state_data["react_item"] do item_id = state_data["react_item"]["item_id"] quantity = state_data["react_item"]["quantity"] || 1 if item_id, do: {item_id, quantity}, else: nil else nil end add_state(acc_stats, state_num, type, react_item, next_state, timeout, can_touch) end) end end