defmodule Odinsea.Game.Reactor do @moduledoc """ Represents a reactor instance on a map. Reactors are map objects (boxes, rocks, plants) that can be hit/activated by players. They have states, can drop items, trigger scripts, and respawn after time. Ported from Java: src/server/maps/MapleReactor.java """ alias Odinsea.Game.ReactorStats @typedoc "Reactor instance struct" @type t :: %__MODULE__{ # Identity oid: integer() | nil, # Object ID (assigned by map) reactor_id: integer(), # Reactor template ID # State state: integer(), # Current state (byte) alive: boolean(), # Whether reactor is active timer_active: boolean(), # Whether timeout timer is running # Position x: integer(), # X position y: integer(), # Y position facing_direction: integer(), # Facing direction (0 or 1) # Properties name: String.t(), # Reactor name delay: integer(), # Respawn delay in milliseconds custom: boolean(), # Custom spawned (not from template) # Stats reference stats: ReactorStats.t() | nil # Template stats } defstruct [ :oid, :reactor_id, :stats, state: 0, alive: true, timer_active: false, x: 0, y: 0, facing_direction: 0, name: "", delay: -1, custom: false ] @doc """ Creates a new reactor instance from template stats. """ @spec new(integer(), ReactorStats.t()) :: t() def new(reactor_id, stats) do %__MODULE__{ reactor_id: reactor_id, stats: stats } end @doc """ Creates a copy of a reactor (for respawning). """ @spec copy(t()) :: t() def copy(reactor) do %__MODULE__{ reactor_id: reactor.reactor_id, stats: reactor.stats, state: 0, alive: true, timer_active: false, x: reactor.x, y: reactor.y, facing_direction: reactor.facing_direction, name: reactor.name, delay: reactor.delay, custom: reactor.custom } end @doc """ Sets the reactor's object ID. """ @spec set_oid(t(), integer()) :: t() def set_oid(reactor, oid) do %{reactor | oid: oid} end @doc """ Sets the reactor's position. """ @spec set_position(t(), integer(), integer()) :: t() def set_position(reactor, x, y) do %{reactor | x: x, y: y} end @doc """ Sets the reactor's state. """ @spec set_state(t(), integer()) :: t() def set_state(reactor, state) do %{reactor | state: state} end @doc """ Sets whether the reactor is alive. """ @spec set_alive(t(), boolean()) :: t() def set_alive(reactor, alive) do %{reactor | alive: alive} end @doc """ Sets the facing direction. """ @spec set_facing_direction(t(), integer()) :: t() def set_facing_direction(reactor, direction) do %{reactor | facing_direction: direction} end @doc """ Sets the reactor name. """ @spec set_name(t(), String.t()) :: t() def set_name(reactor, name) do %{reactor | name: name} end @doc """ Sets the respawn delay. """ @spec set_delay(t(), integer()) :: t() def set_delay(reactor, delay) do %{reactor | delay: delay} end @doc """ Sets whether this is a custom reactor. """ @spec set_custom(t(), boolean()) :: t() def set_custom(reactor, custom) do %{reactor | custom: custom} end @doc """ Sets timer active status. """ @spec set_timer_active(t(), boolean()) :: t() def set_timer_active(reactor, active) do %{reactor | timer_active: active} end @doc """ Gets the reactor type for the current state. Returns the type value or -1 if stats not loaded. """ @spec get_type(t()) :: integer() def get_type(reactor) do if reactor.stats do ReactorStats.get_type(reactor.stats, reactor.state) else -1 end end @doc """ Gets the next state for the current state. """ @spec get_next_state(t()) :: integer() def get_next_state(reactor) do if reactor.stats do ReactorStats.get_next_state(reactor.stats, reactor.state) else -1 end end @doc """ Gets the timeout for the current state. """ @spec get_timeout(t()) :: integer() def get_timeout(reactor) do if reactor.stats do ReactorStats.get_timeout(reactor.stats, reactor.state) else -1 end end @doc """ Gets the touch mode for the current state. Returns: 0 = hit only, 1 = click/touch, 2 = touch only """ @spec can_touch(t()) :: integer() def can_touch(reactor) do if reactor.stats do ReactorStats.can_touch(reactor.stats, reactor.state) else 0 end end @doc """ Gets the required item to react for the current state. Returns {item_id, quantity} or nil. """ @spec get_react_item(t()) :: {integer(), integer()} | nil def get_react_item(reactor) do if reactor.stats do ReactorStats.get_react_item(reactor.stats, reactor.state) else nil end end @doc """ Advances to the next state. Returns the updated reactor. """ @spec advance_state(t()) :: t() def advance_state(reactor) do next_state = get_next_state(reactor) if next_state >= 0 do set_state(reactor, next_state) else reactor end end @doc """ Checks if the reactor should trigger a script for the current state. """ @spec should_trigger_script?(t()) :: boolean() def should_trigger_script?(reactor) do type = get_type(reactor) # Type < 100 or type == 999 typically trigger scripts type < 100 or type == 999 end @doc """ Checks if this reactor is in a looping state (state == next_state). """ @spec is_looping?(t()) :: boolean() def is_looping?(reactor) do reactor.state == get_next_state(reactor) end @doc """ Checks if the reactor should be destroyed (next state is -1 or final state). """ @spec should_destroy?(t()) :: boolean() def should_destroy?(reactor) do next = get_next_state(reactor) next == -1 or get_type(reactor) == 999 end @doc """ Gets the reactor's area of effect (hit box). Returns {tl_x, tl_y, br_x, br_y} or nil if not defined. """ @spec get_area(t()) :: {integer(), integer(), integer(), integer()} | nil def get_area(reactor) do if reactor.stats and reactor.stats.tl and reactor.stats.br do {reactor.stats.tl.x, reactor.stats.tl.y, reactor.stats.br.x, reactor.stats.br.y} else nil end end @doc """ Resets the reactor to initial state (for respawning). """ @spec reset(t()) :: t() def reset(reactor) do %{reactor | state: 0, alive: true, timer_active: false } end end