285 lines
6.6 KiB
Elixir
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
|