module AdventureRL
module Modifiers
# This module is supposed to be included in Mask child classes.
# It will tag that Mask instance as 'solid',
# and check collision with other solid Masks when calling #move_by.
# You can give it a specific solid_tag, which can be passed as
# the :solid_tag key's value upon initialization.
# Multiple solid tags may be passed as an array.
# Solid Masks will only collide with other Solid Masks that have a mutual solid tag.
# The default solid tag is :default.
module Solid
# NOTE: possible :precision_over_performance values:
# :low (or anything other than the higher values)::
# Lowest precision, highest performance.
# Never check every pixel between previous and new positions.
# If there is collision at new position, jump to previous position and return.
# The larger the movement steps, the more distance there will be to the colliding object.
# - -- __CANNOT__ fully close gaps to Solids.
# - -- __CAN__ phase through Solids at high speeds (especially when it lags).
# - -- __CAN__ get stuck in place temporarily when moving on both axes but only colliding on one of them.
# - + Only __one collision check__ per call to #move_by, highest performance.
#
# :medium::
# Medium precision, medium (varying) performance.
# Only checks every pixel in path if the expected destination collides.
# Even then, collision checking is used somewhat sparingly.
# - -- __CAN__ phase through Solids at high speeds (especially when it lags).
# - -- __CAN__ get stuck in place temporarily when moving on both axes but only colliding on one of them.
# - + __CAN__ _almost_ fully close gaps to Solids (no sub-pixel collision checks).
#
# :high::
# High precision, low to medium (varying) performance.
# Only checks every pixel in path if the expected destination collides.
# When checking every pixel in path, check both axes separately, to improve precision.
# - -- __CAN__ phase through Solids at high speeds (especially when it lags).
# - + __CANNOT__ get stuck in place temporarily when moving on both axes but only colliding on one of them.
# - + __CAN__ fully close gaps to Solids.
#
# :highest::
# Highest precision, least performance.
# Always check every pixel between previous and new positions.
# Depending on the amount of (moving) Solid objects on screen,
# - -- Depending on the amount of (moving) Solids,
# this can get very laggy at high speeds =>
# lag produces larger steps (usually), because of Deltatime =>
# larger steps produce more collision checks and more lag.
# - + __CANNOT__ phase through Solids, no matter what the speed is.
# - + __CANNOT__ get stuck in place temporarily when moving on both axes but only colliding on one of them.
# - + __CAN__ fully close gaps to Solids.
DEFAULT_SOLID_SETTINGS = Settings.new(
solid_tag: SolidsManager::DEFAULT_SOLID_TAG,
solid_tag_collides_with: nil,
precision_over_performance: :medium,
static: false,
auto_update: false
)
# Additionally to the Mask's settings Hash or Settings instance,
# you may pass the extra key :solid_tag, to define
# a custom solid tag (or multiple solid tags) upon initialization.
# They are used for collision checking with other Solid Mask objects
# that have a mutual solid tag.
def initialize settings = {}
@settings = DEFAULT_SOLID_SETTINGS.merge settings
@solid_tags = [@settings.get(:solid_tag)].flatten.sort
@solid_tags_collides_with = [@settings.get(:solid_tag_collides_with) || @solid_tags].flatten.sort
@solid_static = @settings.get :static # Basically disables #move_by
@precision_over_performance = @settings.get :precision_over_performance
assign_to_solids_manager if (@settings.get :auto_update)
super @settings
end
def add_to_solids_manager solids_manager
Helpers::Error.error(
"Expected argument to be a SolidsManager, but got",
"'#{solids_manager.inspect}:#{solids_manager.class.name}`."
) unless (solids_manager.is_a? SolidsManager)
@solids_manager = solids_manager
@solids_manager.add_object self, get_solid_tags
end
# Overwrite #set_layer method, so we can get the SolidsManager
# from the Layer, if it has one.
def set_layer layer
super
assign_to_solids_manager if (@settings.get :auto_update)
end
# This method is called when this object is removed from an Inventory.
def removed
remove_from_solids_manager
end
# When it is removed, also remove it from the SolidsManager.
# TODO: Do this properly.
def remove_from_solids_manager
#@solids_manager.remove_object self, [get_solid_tags, get_solid_tags_collides_with].flatten if (@solids_manager)
@solids_manager.remove_object_from_all_quadtrees self if (@solids_manager)
end
# Overwrite #move_by method, so that collision checking with other objects
# with a mutual solid tag is done, and movement prevented if necessary.
def move_by *args
return false if (is_static?)
return super unless (@solids_manager)
@real_point = nil
previous_position = get_position.dup
incremental_position = parse_position(*args)
expected_position = {
x: (previous_position[:x] + (incremental_position.key?(:x) ? incremental_position[:x] : 0)),
y: (previous_position[:y] + (incremental_position.key?(:y) ? incremental_position[:y] : 0))
}
# NOTE:
# This is a bit of a hacky workaround for some
# weird Pusher behavior with Velocity and Gravity.
previous_precision_over_performance = @precision_over_performance.dup
opts = args.last.is_a?(Hash) ? args.last : nil
@precision_over_performance = opts[:precision_over_performance] if (opts.key? :precision_over_performance)
if ([:highest].include? @precision_over_performance)
move_by_steps incremental_position
else
@position[:x] += incremental_position[:x] if (incremental_position.key? :x)
@position[:y] += incremental_position[:y] if (incremental_position.key? :y)
# TODO
#puts 'PUSHING' if (is_a?(Player) && opts && opts[:pushed_by_pusher])
unless (move_by_handle_collision_with_previous_position previous_position)
move_by_steps incremental_position if ([:medium, :high].include? @precision_over_performance)
end
end
@precision_over_performance = previous_precision_over_performance
@solids_manager.reset_object self, get_solid_tags unless (@position == previous_position)
return @position == expected_position
end
# Overwrite the #move_to method, so we can
# reset the object for the solids_manager if necessary.
def move_to *args
previous_position = get_position.dup
super
return unless (@solids_manager)
@solids_manager.reset_object self, get_solid_tags if (@position != previous_position)
end
# Returns true if this Mask is currently in collision
# with another solid Mask which has a mutual solid tag.
# TODO: Write documentation for callback method.
def in_collision?
if (@solids_manager)
is_colliding = @solids_manager.collides?(self, get_solid_tags_collides_with)
else
is_colliding = false
end
is_colliding if (is_colliding && methods.include?(:is_colliding))
return is_colliding
end
# Returns all currently colliding objects (if any).
# TODO: Write documentation for callback method.
def get_colliding_objects
if (@solids_manager)
colliding_objects = @solids_manager.get_colliding_objects(self, get_solid_tags_collides_with)
else
colliding_objects = []
end
is_colliding_with_objects colliding_objects if (colliding_objects.any? && methods.include?(:is_colliding_with_objects))
return colliding_objects
end
# Makes this Solid Mask static.
def make_static
@solid_static = true
end
# Returns true if this is a static solid Mask,
# which means it cannot be moved with #move_by.
def is_static?
return !!@solid_static
end
def set_solid_tags *new_solid_tags
@solids_manager.remove_object self, get_solid_tags if (@solids_manager)
@solid_tags = [new_solid_tags].flatten.compact
@solids_manager.add_object self, get_solid_tags if (@solids_manager)
end
def set_solid_tags_collides_with *new_solid_tags_collides_with
@solid_tags_collides_with = [new_solid_tags_collides_with].flatten.compact
end
# Returns this Mask's solid tags,
# which other Masks use to check collision against _this_ Mask.
def get_solid_tags
return @solid_tags
end
# Returns the solid tags,
# which this Mask uses to check collision against _other_ Masks.
def get_solid_tags_collides_with
return @solid_tags_collides_with || @solid_tags
end
private
def assign_to_solids_manager
layer = get_layer
return unless (layer && layer.has_solids_manager?)
@solids_manager = layer.get_solids_manager
@solids_manager.add_object self, get_solid_tags if (@solids_manager)
end
# This is the ugliest method in the project.
# I can live with there being __one__ ugly method.
# Also, it _does_ do some complicated stuff, so cut it some slack.
# It didn't ask to be this way.
def move_by_steps incremental_position
incremental_position[:x] ||= 0
incremental_position[:y] ||= 0
larger_axis = :x if (incremental_position[:x].abs >= incremental_position[:y].abs)
larger_axis = :y if (incremental_position[:y].abs > incremental_position[:x].abs)
smaller_axis = (larger_axis == :x) ? :y : :x
larger_axis_sign = incremental_position[larger_axis].sign
smaller_axis_sign = incremental_position[smaller_axis].sign
smaller_axis_increment_at = (incremental_position[larger_axis].abs.to_f / incremental_position[smaller_axis].abs.to_f).round rescue nil
remaining_values = {
larger_axis => ((incremental_position[larger_axis].abs % 1) * larger_axis_sign),
smaller_axis => ((incremental_position[smaller_axis].abs % 1) * smaller_axis_sign),
}
return unless (move_by_steps_for_remaining_values remaining_values)
# NOTE
# We use #to_i here, because a negative float's #floor method decreases its value. Example:
# 1.75.floor # => 1.0
# -1.75.floor # => -2.0
# 1.75.to_i # => 1.0
# -1.75.to_i # => -1.0
incremental_position[larger_axis].to_i.abs.times do |axis_index|
initial_previous_position = @position.dup
tmp_in_collision_count = 0
previous_position = @position.dup
@position[larger_axis] += larger_axis_sign
tmp_in_collision_count += 1 unless (
move_by_handle_collision_with_previous_position(previous_position)
) if ([:high, :highest].include? @precision_over_performance)
if (smaller_axis_increment_at &&
(((axis_index + 1) % smaller_axis_increment_at) == 0)
)
previous_position = @position.dup
@position[smaller_axis] += smaller_axis_sign
tmp_in_collision_count += 1 unless (
move_by_handle_collision_with_previous_position(previous_position)
) if ([:high, :highest].include? @precision_over_performance)
end
return unless (tmp_in_collision_count < 2)
if ([:medium].include? @precision_over_performance)
return unless (move_by_handle_collision_with_previous_position initial_previous_position)
end
end
end
def move_by_steps_for_remaining_values remaining_values
return true unless ([:high, :highest].include? @precision_over_performance)
return true if (remaining_values.values.all? { |val| val == 0 })
tmp_in_collision_count = 0
remaining_values.each do |remaining_axis, remaining_value|
next if (remaining_value == 0)
previous_position = @position.dup
@position[remaining_axis] += remaining_value
remaining_values[remaining_axis] = 0
unless (move_by_handle_collision_with_previous_position previous_position)
tmp_in_collision_count += 1
next # break
end
end
return false if (tmp_in_collision_count == 2) # NOTE: Slight performance improvement
return true
end
# Returns true if there was no collision, and
# returns false if there was and it had to reset to the previous_position.
def move_by_handle_collision_with_previous_position previous_position
if (in_collision?)
@position = previous_position
return false
end
return true
end
end
end
end