Files
odinsea-elixir/lib/odinsea/game/reactor.ex
2026-02-14 23:12:33 -07:00

285 lines
6.6 KiB
Elixir

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