class Natural20::BattleMap

Attributes

area_triggers[R]
base_map[R]
entities[R]
feet_per_grid[R]
interactable_objects[R]
properties[R]
session[R]
size[R]
spawn_points[R]
tokens[R]
unaware_npcs[R]

Public Class Methods

new(session, map_file) click to toggle source

@param session [Natural20::Session] The current game session @param map_file [String] Path to map file

# File lib/natural_20/battle_map.rb, line 12
def initialize(session, map_file)
  @session = session
  @map_file = map_file
  @spawn_points = {}
  @entities = {}
  @area_triggers = {}
  @interactable_objects = {}
  @unaware_npcs = []

  @properties = YAML.load_file(File.join(session.root_path, "#{map_file}.yml")).deep_symbolize_keys!

  @feet_per_grid = @properties[:grid_size] || 5

  # terrain layer
  @base_map = @properties.dig(:map, :base).map do |lines|
    lines.each_char.map.to_a
  end.transpose
  @size = [@base_map.size, @base_map.first.size]

  # terrain layer 2
  @base_map_1 = if @properties.dig(:map, :base_1).blank?
                  @size[0].times.map do
                    @size[1].times.map { nil }
                  end
                else
                  @properties.dig(:map, :base_1).map do |lines|
                    lines.each_char.map { |c| c == '.' ? nil : c }
                  end.transpose
                end

  # meta layer
  if @properties.dig(:map, :meta)
    @meta_map = @properties.dig(:map, :meta).map do |lines|
      lines.each_char.map.to_a
    end.transpose
  end

  @legend = @properties[:legend] || {}

  @tokens = @size[0].times.map do
    @size[1].times.map { nil }
  end

  @area_notes = @size[0].times.map do
    @size[1].times.map { nil }
  end

  @objects = @size[0].times.map do
    @size[1].times.map do
      []
    end
  end

  @properties[:notes]&.each_with_index do |note, index|
    note_object = OpenStruct.new(note.merge(note_id: note[:id] || index))
    note_positions = case note_object.type
                     when 'point'
                       note[:positions]
                     when 'rectangle'
                       note[:positions].map do |position|
                         left_x, left_y, right_x, right_y = position

                         (left_x..right_x).map do |pos_x|
                           (left_y..right_y).map do |pos_y|
                             [pos_x, pos_y]
                           end
                         end
                       end.flatten(2).uniq
                     else
                       raise "invalid note type #{note_object.type}"
                     end

    note_positions.each do |position|
      pos_x, pos_y = position
      @area_notes[pos_x][pos_y] ||= []
      @area_notes[pos_x][pos_y] << note_object unless @area_notes[pos_x][pos_y].include?(note_object)
    end
  end

  @triggers = (@properties[:triggers] || {}).deep_symbolize_keys

  @light_builder = Natural20::StaticLightBuilder.new(self)

  setup_objects
  setup_npcs
  compute_lights # compute static lights
end

Public Instance Methods

activate_map_triggers(trigger_type, source, opt = {}) click to toggle source
# File lib/natural_20/battle_map.rb, line 100
def activate_map_triggers(trigger_type, source, opt = {})
  return unless @triggers.key?(trigger_type.to_sym)

  @triggers[trigger_type.to_sym].each do |trigger|
    next if trigger[:if] && !source&.eval_if(trigger[:if], opt)

    case trigger[:type]
    when 'message'
      opt[:ui_controller]&.show_message(trigger[:content])
    when 'battle_end'
      return :battle_end
    else
      raise "unknown trigger type #{trigger[:type]}"
    end
  end
end
add(entity, pos_x, pos_y, group: :b) click to toggle source

@param entity [Natural20::Battle] @param pos_x [Integer] @param pos_y [Integer] @param group [Symbol]

# File lib/natural_20/battle_map.rb, line 794
def add(entity, pos_x, pos_y, group: :b)
  @unaware_npcs << { group: group&.to_sym || :b, entity: entity }
  @entities[entity] = [pos_x, pos_y]
  place(pos_x, pos_y, entity, nil)
end
area_trigger!(entity, position, is_flying) click to toggle source
# File lib/natural_20/battle_map.rb, line 735
def area_trigger!(entity, position, is_flying)
  trigger_results = @area_triggers.map do |k, _prop|
    next if k.dead?

    k.area_trigger_handler(entity, position, is_flying)
  end.flatten.compact

  trigger_results.uniq
end
can_see?(entity, entity2, distance: nil, entity_1_pos: nil, entity_2_pos: nil, allow_dark_vision: true, active_perception: 0, active_perception_disadvantage: 0) click to toggle source

Checks if an entity can see another @param entity [Natural20::Entity] entity looking @param entity2 [Natural20::Entity] entity being looked at @param distance [Integer] @param entity_2_pos [Array] position override for entity2 @param allow_dark_vision [Boolean] Allow darkvision @return [Boolean]

# File lib/natural_20/battle_map.rb, line 372
def can_see?(entity, entity2, distance: nil, entity_1_pos: nil, entity_2_pos: nil, allow_dark_vision: true, active_perception: 0, active_perception_disadvantage: 0)
  raise 'invalid entity passed' if @entities[entity].nil? && @interactable_objects[entity].nil?

  entity_1_squares = entity_1_pos ? entity_squares_at_pos(entity, *entity_1_pos) : entity_squares(entity)
  entity_2_squares = entity_2_pos ? entity_squares_at_pos(entity2, *entity_2_pos) : entity_squares(entity2)

  has_line_of_sight = false
  max_illumniation = 0.0
  sighting_distance = nil

  entity_1_squares.each do |pos1|
    entity_2_squares.each do |pos2|
      pos1_x, pos1_y = pos1
      pos2_x, pos2_y = pos2
      next if pos1_x >= size[0] || pos1_x.negative? || pos1_y >= size[1] || pos1_y.negative?
      next if pos2_x >= size[0] || pos2_x.negative? || pos2_y >= size[1] || pos2_y.negative?
      next unless line_of_sight?(pos1_x, pos1_y, pos2_x, pos2_y, distance)

      location_illumnination = light_at(pos2_x, pos2_y)
      max_illumniation = [location_illumnination, max_illumniation].max
      sighting_distance = Math.sqrt((pos1_x - pos2_x)**2 + (pos1_y - pos2_y)**2).floor
      has_line_of_sight = true
    end
  end

  if has_line_of_sight && max_illumniation < 0.5
    return allow_dark_vision && entity.darkvision?(sighting_distance * @feet_per_grid)
  end

  has_line_of_sight
end
can_see_square?(entity, pos2_x, pos2_y, allow_dark_vision: true) click to toggle source

Test to see if an entity can see a square @param entity [Natural20::Entity] @param pos2_x [Integer] @param pos2_y [Integer] @param allow_dark_vision [Boolean] Allow darkvision @return [Boolean]

# File lib/natural_20/battle_map.rb, line 341
def can_see_square?(entity, pos2_x, pos2_y, allow_dark_vision: true)
  has_line_of_sight = false
  max_illumniation = 0.0
  sighting_distance = nil

  entity_1_squares = entity_squares(entity)
  entity_1_squares.each do |pos1|
    pos1_x, pos1_y = pos1
    return true if [pos1_x, pos1_y] == [pos2_x, pos2_y]
    next unless line_of_sight?(pos1_x, pos1_y, pos2_x, pos2_y, nil, false)

    location_illumnination = light_at(pos2_x, pos2_y)
    max_illumniation = [location_illumnination, max_illumniation].max
    sighting_distance = Math.sqrt((pos1_x - pos2_x)**2 + (pos1_y - pos2_y)**2).floor
    has_line_of_sight = true
  end

  if has_line_of_sight && max_illumniation < 0.5
    return allow_dark_vision && entity.darkvision?(sighting_distance * @feet_per_grid)
  end

  has_line_of_sight
end
cover_at(pos_x, pos_y, entity = false) click to toggle source

@param pos_x [Integer] @param pos_y [Integer] @param entity [Boolean] inlcude entities

# File lib/natural_20/battle_map.rb, line 668
def cover_at(pos_x, pos_y, entity = false)
  return :half if object_at(pos_x, pos_y)&.half_cover?
  return :three_quarter if object_at(pos_x, pos_y)&.three_quarter_cover?
  return :total if object_at(pos_x, pos_y)&.total_cover?

  return entity_at(pos_x, pos_y).size_identifier if entity && entity_at(pos_x, pos_y)

  :none
end
difficult_terrain?(entity, pos_x, pos_y, _battle = nil) click to toggle source

Determines if terrain is a difficult terrain @param entity [Natural20::Entity] @param pos_x [Integer] @param pos_y [Integer] @param battle [Natural20::Battle] @return [Boolean] Returns if difficult terrain or not

# File lib/natural_20/battle_map.rb, line 614
def difficult_terrain?(entity, pos_x, pos_y, _battle = nil)
  entity_squares_at_pos(entity, pos_x, pos_y).each do |pos|
    r_x, r_y = pos
    next if @tokens[r_x][r_y] && @tokens[r_x][r_y][:entity] == entity
    return true if @tokens[r_x][r_y] && !@tokens[r_x][r_y][:entity].dead?
    return true if object_at(r_x, r_y) && object_at(r_x, r_y)&.movement_cost > 1
  end

  false
end
distance(entity1, entity2, entity_1_pos: nil, entity_2_pos: nil) click to toggle source

Computes the distance between two entities @param entity1 [Natural20::Entity] @param entity2 [Natural20::Entity] @result [Integer]

# File lib/natural_20/battle_map.rb, line 231
def distance(entity1, entity2, entity_1_pos: nil, entity_2_pos: nil)
  raise 'entity 1 param cannot be nil' if entity1.nil?
  raise 'entity 2 param cannot be nil' if entity2.nil?

  # entity 1 squares
  entity_1_sq = entity_1_pos ? entity_squares_at_pos(entity1, *entity_1_pos) : entity_squares(entity1)
  entity_2_sq = entity_2_pos ? entity_squares_at_pos(entity2, *entity_2_pos) : entity_squares(entity2)

  entity_1_sq.map do |ent1_pos|
    entity_2_sq.map do |ent2_pos|
      pos1_x, pos1_y = ent1_pos
      pos2_x, pos2_y = ent2_pos
      Math.sqrt((pos1_x - pos2_x)**2 + (pos1_y - pos2_y)**2).floor
    end
  end.flatten.min
end
entity_at(pos_x, pos_y) click to toggle source

Get entity at map location @param pos_x [Integer] @param pos_y [Integer] @return [Natural20::Entity]

# File lib/natural_20/battle_map.rb, line 420
def entity_at(pos_x, pos_y)
  entity_data = @tokens[pos_x][pos_y]
  return nil if entity_data.nil?

  entity_data[:entity]
end
entity_or_object_pos(thing) click to toggle source

@param thing [Natural20::Entity] @return [Array<Integer,Integer>]

# File lib/natural_20/battle_map.rb, line 747
def entity_or_object_pos(thing)
  thing.is_a?(ItemLibrary::Object) ? @interactable_objects[thing] : @entities[thing]
end
entity_squares(entity, squeeze = false) click to toggle source

Get all the location of the squares occupied by said entity (e.g. for large, huge creatures) @param entity [Natural20::Entity] @return [Array]

# File lib/natural_20/battle_map.rb, line 265
def entity_squares(entity, squeeze = false)
  raise 'invalid entity' unless entity

  pos1_x, pos1_y = entity_or_object_pos(entity)
  entity_1_squares = []
  token_size = if squeeze
                 [entity.token_size - 1, 1].max
               else
                 entity.token_size
               end
  (0...token_size).each do |ofs_x|
    (0...token_size).each do |ofs_y|
      next if (pos1_x + ofs_x >= size[0]) || (pos1_y + ofs_y >= size[1])

      entity_1_squares << [pos1_x + ofs_x, pos1_y + ofs_y]
    end
  end
  entity_1_squares
end
entity_squares_at_pos(entity, pos1_x, pos1_y, squeeze = false) click to toggle source

Get all the location of the squares occupied by said entity (e.g. for large, huge creatures) and use specified entity location instead of current location on the map @param entity [Natural20::Entity] @param pos1_x [Integer] @param pos1_y [Integer] @return [Array]

# File lib/natural_20/battle_map.rb, line 291
def entity_squares_at_pos(entity, pos1_x, pos1_y, squeeze = false)
  entity_1_squares = []
  token_size = if squeeze
                 [entity.token_size - 1, 1].max
               else
                 entity.token_size
               end
  (0...token_size).each do |ofs_x|
    (0...token_size).each do |ofs_y|
      next if (pos1_x + ofs_x >= size[0]) || (pos1_y + ofs_y >= size[1])

      entity_1_squares << [pos1_x + ofs_x, pos1_y + ofs_y]
    end
  end
  entity_1_squares
end
ground_at(pos_x, pos_y) click to toggle source
# File lib/natural_20/battle_map.rb, line 185
def ground_at(pos_x, pos_y)
  available_objects = objects_at(pos_x, pos_y).compact
  available_objects.detect { |obj| obj.is_a?(ItemLibrary::Ground) }
end
highlight(source, perception_check) click to toggle source

highlights objects of interest if enabled on object @param source [Natural20::Entity] entity that is observing @param perception_check [Integer] Perception value @returns [Hash] objects of interest with notes

# File lib/natural_20/battle_map.rb, line 682
def highlight(source, perception_check)
  (@entities.keys + interactable_objects.keys).map do |entity|
    next if source == entity
    next unless can_see?(source, entity)

    perception_key = "#{source.entity_uid}_#{entity.entity_uid}"
    perception_check = @session.load_state(:perception).fetch(perception_key, perception_check)
    @session.save_state(:perception, { perception_key => perception_check })
    highlighted_notes = entity.try(:list_notes, source, perception_check, highlight: true) || []

    next if highlighted_notes.empty?

    [entity, highlighted_notes]
  end.compact.to_h
end
items_on_the_ground(entity) click to toggle source
# File lib/natural_20/battle_map.rb, line 168
def items_on_the_ground(entity)
  target_squares = entity.melee_squares(self)
  target_squares += entity_squares(entity)

  available_objects = target_squares.map do |square|
    objects_at(*square)
  end.flatten.compact

  ground_objects = available_objects.select { |obj| obj.is_a?(ItemLibrary::Ground) }
  ground_objects.map do |obj|
    items = obj.inventory.select { |o| o.qty.positive? }
    next if items.empty?

    [obj, items]
  end.compact
end
jump_required?(entity, pos_x, pos_y) click to toggle source
# File lib/natural_20/battle_map.rb, line 625
def jump_required?(entity, pos_x, pos_y)
  entity_squares_at_pos(entity, pos_x, pos_y).each do |pos|
    r_x, r_y = pos
    next if @tokens[r_x][r_y] && @tokens[r_x][r_y][:entity] == entity
    return true if object_at(r_x, r_y) && object_at(r_x, r_y)&.jump_required?
  end

  false
end
light_at(pos_x, pos_y) click to toggle source
# File lib/natural_20/battle_map.rb, line 404
def light_at(pos_x, pos_y)
  if @light_map
    @light_map[pos_x][pos_y] + @light_builder.light_at(pos_x, pos_y)
  else
    @light_builder.light_at(pos_x, pos_y)
  end
end
line_distance(entity1, pos2_x, pos2_y, entity_1_pos: nil) click to toggle source

Returns the line distance between an entity and a location @param entity [Natural20::Entity] @param pos2_x [Integer] @param pos2_y [Integer] @result [Integer]

# File lib/natural_20/battle_map.rb, line 253
def line_distance(entity1, pos2_x, pos2_y, entity_1_pos: nil)
  entity_1_sq = entity_1_pos ? entity_squares_at_pos(entity1, *entity_1_pos) : entity_squares(entity1)

  entity_1_sq.map do |ent1_pos|
    pos1_x, pos1_y = ent1_pos
    Math.sqrt((pos1_x - pos2_x)**2 + (pos1_y - pos2_y)**2).floor
  end.flatten.min
end
line_of_sight?(pos1_x, pos1_y, pos2_x, pos2_y, distance = nil, inclusive = false, entity = false) click to toggle source

Computes one of sight between two points @param pos1_x [Integer] @param pos1_y [Integer] @param pos2_x [Integer] @param pos2_y [Integer] @param distance [Integer] @return [Array<Array<Integer,Integer>>] Cover characteristics if there is LOS

# File lib/natural_20/battle_map.rb, line 654
def line_of_sight?(pos1_x, pos1_y, pos2_x, pos2_y, distance = nil, inclusive = false, entity = false)
  squares = squares_in_path(pos1_x, pos1_y, pos2_x, pos2_y, inclusive: inclusive)
  squares.each_with_index.map do |s, index|
    return nil if distance && index == (distance - 1)
    return nil if opaque?(*s)
    return nil if cover_at(*s) == :total

    [cover_at(*s, entity), s]
  end
end
line_of_sight_for?(entity, pos2_x, pos2_y, distance = nil) click to toggle source

Compute if entity is in line of sight @param entity [Natural20::Entity] @param pos2_x [Integer] @param pos2_y [Integer] @param distance [Integer] @return [TrueClass, FalseClass]

# File lib/natural_20/battle_map.rb, line 328
def line_of_sight_for?(entity, pos2_x, pos2_y, distance = nil)
  raise 'cannot find entity' if @entities[entity].nil?

  pos1_x, pos1_y = @entities[entity]
  line_of_sight?(pos1_x, pos1_y, pos2_x, pos2_y, distance)
end
look(entity, distance = nil) click to toggle source

Natural20::Entity to look around @param entity [Natural20::Entity] The entity to look around his line of sight @return [Hash] entities in line of sight

# File lib/natural_20/battle_map.rb, line 311
def look(entity, distance = nil)
  @entities.map do |k, v|
    next if k == entity

    pos1_x, pos1_y = v
    next unless can_see?(entity, k, distance: distance)

    [k, [pos1_x, pos1_y]]
  end.compact.to_h
end
move_to!(entity, pos_x, pos_y, battle) click to toggle source

Moves an entity to a specified location on the board @param entity [Natural20::Entity] @param pos_x [Integer] @param pos_y [Integer] @param battle [Natural20::Battle]

# File lib/natural_20/battle_map.rb, line 444
def move_to!(entity, pos_x, pos_y, battle)
  cur_x, cur_y = @entities[entity]

  entity_data = @tokens[cur_x][cur_y]

  source_token_size = if requires_squeeze?(entity, cur_x, cur_y, self, battle)
                        [entity.token_size - 1, 1].max
                      else
                        entity.token_size
                      end

  destination_token_size = if requires_squeeze?(entity, pos_x, pos_y, self, battle)
                             entity.squeezed!
                             [entity.token_size - 1, 1].max
                           else
                             entity.unsqueeze
                             entity.token_size
                           end

  (0...source_token_size).each do |ofs_x|
    (0...source_token_size).each do |ofs_y|
      @tokens[cur_x + ofs_x][cur_y + ofs_y] = nil
    end
  end

  (0...destination_token_size).each do |ofs_x|
    (0...destination_token_size).each do |ofs_y|
      @tokens[pos_x + ofs_x][pos_y + ofs_y] = entity_data
    end
  end

  @entities[entity] = [pos_x, pos_y]
end
movement_cost(entity, path, battle = nil, manual_jump = []) click to toggle source

@param entity [Natural20::Entity] @param path [Array] @param battle [Natural20::Battle] @param manual_jump [Array] @return [Natural20::MovementHelper::Movement]

# File lib/natural_20/battle_map.rb, line 492
def movement_cost(entity, path, battle = nil, manual_jump = [])
  return Natural20::MovementHelper::Movement.empty if path.empty?

  budget = entity.available_movement(battle) / @feet_per_grid
  compute_actual_moves(entity, path, self, battle, budget, test_placement: false,
                                                           manual_jump: manual_jump)
end
object_at(pos_x, pos_y, reveal_concealed: false) click to toggle source

Get object at map location @param pos_x [Integer] @param pos_y [Integer] @param reveal_concealed [Boolean] @return [ItemLibrary::Object]

# File lib/natural_20/battle_map.rb, line 131
def object_at(pos_x, pos_y, reveal_concealed: false)
  @objects[pos_x][pos_y]&.detect { |o| reveal_concealed || !o.concealed? }
end
objects_at(pos_x, pos_y) click to toggle source

Get object at map location @param pos_x [Integer] @param pos_y [Integer] @return [Array<ItemLibrary::Object>]

# File lib/natural_20/battle_map.rb, line 139
def objects_at(pos_x, pos_y)
  @objects[pos_x][pos_y]
end
objects_near(entity, battle = nil) click to toggle source

Lists interactable objects near an entity @param entity [Entity] @param battle [Natural20::Battle] @return [Array]

# File lib/natural_20/battle_map.rb, line 147
def objects_near(entity, battle = nil)
  target_squares = entity.melee_squares(self)
  target_squares += battle.map.entity_squares(entity) if battle&.map
  objects = []

  available_objects = target_squares.map do |square|
    objects_at(*square)
  end.flatten.compact

  available_objects.each do |object|
    objects << object unless object.available_interactions(entity, battle).empty?
  end

  @entities.each do |object, position|
    next if object == entity

    objects << object if !object.available_interactions(entity, battle).empty? && target_squares.include?(position)
  end
  objects
end
opaque?(pos_x, pos_y) click to toggle source

check if this interrupts line of sight (not necessarily movement)

# File lib/natural_20/battle_map.rb, line 636
def opaque?(pos_x, pos_y)
  case (@base_map[pos_x][pos_y])
  when '#'
    true
  when '.'
    false
  else
    object_at(pos_x, pos_y)&.opaque?
  end
end
passable?(entity, pos_x, pos_y, battle = nil, allow_squeeze = true) click to toggle source

Describes if terrain is passable or not @param entity [Natural20::Entity] @param pos_x [Integer] @param pos_y [Integer] @param battle [Natural20::Battle] @param allow_squeeze [Boolean] Allow entity to squeeze inside a space (PHB ) @return [Boolean]

# File lib/natural_20/battle_map.rb, line 507
def passable?(entity, pos_x, pos_y, battle = nil, allow_squeeze = true)
  effective_token_size = if allow_squeeze
                           [entity.token_size - 1, 1].max
                         else
                           entity.token_size
                         end

  (0...effective_token_size).each do |ofs_x|
    (0...effective_token_size).each do |ofs_y|
      relative_x = pos_x + ofs_x
      relative_y = pos_y + ofs_y

      return false if relative_x >= @size[0]
      return false if relative_y >= @size[1]

      return false if @base_map[relative_x][relative_y] == '#'
      return false if object_at(relative_x, relative_y) && !object_at(relative_x, relative_y).passable?

      next unless battle && @tokens[relative_x][relative_y]

      location_entity = @tokens[relative_x][relative_y][:entity]

      next if @tokens[relative_x][relative_y][:entity] == entity
      next unless battle.opposing?(location_entity, entity)
      next if location_entity.dead? || location_entity.unconscious?
      if entity.class_feature?('halfling_nimbleness') && (location_entity.size_identifier - entity.size_identifier) >= 1
        next
      end
      if battle.opposing?(location_entity,
                          entity) && (location_entity.size_identifier - entity.size_identifier).abs < 2
        return false
      end
    end
  end

  true
end
perception_on(entity, source, perception_check) click to toggle source

Reads perception related notes on an entity @param entity [Object] The target to percive on @param source [Natural20::Entity] Entity perceiving @param perception_check [Integer] Perception roll of source @return [Array] Array of notes

# File lib/natural_20/battle_map.rb, line 703
def perception_on(entity, source, perception_check)
  perception_key = "#{source.entity_uid}_#{entity.entity_uid}"
  perception_check = @session.load_state(:perception).fetch(perception_key, perception_check)
  @session.save_state(:perception, { perception_key => perception_check })
  entity.try(:list_notes, source, perception_check) || []
end
perception_on_area(pos_x, pos_y, source, perception_check) click to toggle source

Reads perception related notes on an area @param pos_x [Integer] @param pos_y [Integer] @param source [Natural20::Entity] @param perception_check [Integer] Perception roll of source @return [Array] Array of notes

# File lib/natural_20/battle_map.rb, line 716
def perception_on_area(pos_x, pos_y, source, perception_check)
  notes = @area_notes[pos_x][pos_y] || []

  notes.map do |note_object|
    note_object.notes.each_with_index.map do |note, index|
      perception_key = "#{source.entity_uid}_#{note_object.note_id}_#{index}"
      perception_check = @session.load_state(:perception).fetch(perception_key, perception_check)
      @session.save_state(:perception, { perception_key => perception_check })
      next if note[:perception_dc] && perception_check <= note[:perception_dc]

      if note[:perception_dc] && note[:perception_dc] != 0
        t('perception.passed', note: note[:note])
      else
        note[:note]
      end
    end
  end.flatten.compact
end
place(pos_x, pos_y, entity, token = nil, battle = nil) click to toggle source

Place token here if it is not already present on the board @param pos_x [Integer] @param pos_y [Integer] @param entity [Natural20::Entity] @param token [String] @param battle [Natural20::Battle]

# File lib/natural_20/battle_map.rb, line 196
def place(pos_x, pos_y, entity, token = nil, battle = nil)
  raise 'entity param is required' if entity.nil?

  entity_data = { entity: entity, token: token || entity.name&.first }
  @tokens[pos_x][pos_y] = entity_data
  @entities[entity] = [pos_x, pos_y]

  source_token_size = if requires_squeeze?(entity, pos_x, pos_y, self, battle)
                        entity.squeezed!
                        [entity.token_size - 1, 1].max
                      else
                        entity.token_size
                      end

  (0...source_token_size).each do |ofs_x|
    (0...source_token_size).each do |ofs_y|
      @tokens[pos_x + ofs_x][pos_y + ofs_y] = entity_data
    end
  end
end
place_at_spawn_point(position, entity, token = nil, battle = nil) click to toggle source
# File lib/natural_20/battle_map.rb, line 217
def place_at_spawn_point(position, entity, token = nil, battle = nil)
  unless @spawn_points.key?(position.to_s)
    raise "unknown spawn position #{position}. should be any of #{@spawn_points.keys.join(',')}"
  end

  pos_x, pos_y = @spawn_points[position.to_s][:location]
  place(pos_x, pos_y, entity, token, battle)
  EventManager.logger.debug "place #{entity.name} at #{pos_x}, #{pos_y}"
end
place_object(object_info, pos_x, pos_y, object_meta = {}) click to toggle source

Places an object onto the map @param object_info [Hash] @param pos_x [Integer] @param pos_y [Integer] @param object_meta [Hash] @return [ItemLibrary::Object]

# File lib/natural_20/battle_map.rb, line 757
def place_object(object_info, pos_x, pos_y, object_meta = {})
  return if object_info.nil?

  obj = if object_info.is_a?(ItemLibrary::Object)
          object_info
        elsif object_info[:item_class]
          item_klass = object_info[:item_class].constantize

          item_obj = item_klass.new(self, object_info.merge(object_meta))
          @area_triggers[item_obj] = {} if item_klass.included_modules.include?(ItemLibrary::AreaTrigger)

          item_obj
        else
          ItemLibrary::Object.new(self, object_meta.merge(object_info))
        end

  @interactable_objects[obj] = [pos_x, pos_y]

  if obj.token.is_a?(Array)
    obj.token.each_with_index do |line, y|
      line.each_char.map.to_a.each_with_index do |t, x|
        next if t == '.' # ignore mask

        @objects[pos_x + x][pos_y + y] << obj
      end
    end
  else
    @objects[pos_x][pos_y] << obj
  end

  obj
end
placeable?(entity, pos_x, pos_y, battle = nil, squeeze = true) click to toggle source

Determines if it is possible to place a token in this location @param entity [Natural20::Entity] @param pos_x [Integer] @param pos_y [Integer] @param battle [Natural20::Battle] @return [Boolean]

# File lib/natural_20/battle_map.rb, line 594
def placeable?(entity, pos_x, pos_y, battle = nil, squeeze = true)
  return false unless passable?(entity, pos_x, pos_y, battle, squeeze)

  entity_squares_at_pos(entity, pos_x, pos_y, squeeze).each do |pos|
    p_x, p_y = pos
    next if @tokens[p_x][p_y] && @tokens[p_x][p_y][:entity] == entity
    return false if @tokens[p_x][p_y] && !@tokens[p_x][p_y][:entity].dead?
    return false if object_at(p_x, p_y) && !object_at(p_x, p_y)&.passable?
    return false if object_at(p_x, p_y) && !object_at(p_x, p_y)&.placeable?
  end

  true
end
position_of(entity) click to toggle source
# File lib/natural_20/battle_map.rb, line 412
def position_of(entity)
  entity.is_a?(ItemLibrary::Object) ? interactable_objects[entity] : @entities[entity]
end
squares_in_path(pos1_x, pos1_y, pos2_x, pos2_y, distance: nil, inclusive: true) click to toggle source
# File lib/natural_20/battle_map.rb, line 545
def squares_in_path(pos1_x, pos1_y, pos2_x, pos2_y, distance: nil, inclusive: true)
  if [pos1_x, pos1_y] == [pos2_x, pos2_y]
    return inclusive ? [[pos1_x, pos1_y]] : []
  end

  arrs = []
  if pos2_x == pos1_x
    scanner = pos2_y > pos1_y ? (pos1_y...pos2_y) : (pos2_y...pos1_y)

    scanner.each_with_index do |y, index|
      break if !distance.nil? && index >= distance
      next if !inclusive && ((y == pos1_y) || (y == pos2_y))

      arrs << [pos1_x, y]
    end
  else
    m = (pos2_y - pos1_y).to_f / (pos2_x - pos1_x)
    scanner = pos2_x > pos1_x ? (pos1_x...pos2_x) : (pos2_x...pos1_x)
    if m.zero?
      scanner.each_with_index do |x, index|
        break if !distance.nil? && index >= distance
        next if !inclusive && ((x == pos1_x) || (x == pos2_x))

        arrs << [x, pos2_y]
      end
    else
      b = pos1_y - m * pos1_x
      step = m.abs > 1 ? 1 / m.abs : m.abs

      scanner.step(step).each_with_index do |x, index|
        y = (m * x + b).round

        break if !distance.nil? && index >= distance
        next if !inclusive && ((x.round == pos1_x && y == pos1_y) || (x.round == pos2_x && y == pos2_y))

        arrs << [x.round, y]
      end
    end
  end

  arrs.uniq
end
thing_at(pos_x, pos_y, reveal_concealed: false) click to toggle source

Get entity or object at map location @param pos_x [Integer] @param pos_y [Integer] @param reveal_concealed [Boolean] Included concealed objects @return [Array<Nautral20::Entity>]

# File lib/natural_20/battle_map.rb, line 432
def thing_at(pos_x, pos_y, reveal_concealed: false)
  things = []
  things << entity_at(pos_x, pos_y)
  things << object_at(pos_x, pos_y, reveal_concealed: reveal_concealed)
  things.compact
end
valid_position?(pos_x, pos_y) click to toggle source
# File lib/natural_20/battle_map.rb, line 478
def valid_position?(pos_x, pos_y)
  return false if pos_x >= @base_map.size || pos_x.negative? || pos_y >= @base_map[0].size || pos_y.negative?

  return false if @base_map[pos_x][pos_y] == '#'
  return false unless @tokens[pos_x][pos_y].nil?

  true
end
wall?(pos_x, pos_y) click to toggle source
# File lib/natural_20/battle_map.rb, line 117
def wall?(pos_x, pos_y)
  return true if pos_x.negative? || pos_y.negative?
  return true if pos_x >= size[0] || pos_y >= size[1]

  return true if object_at(pos_x, pos_y)&.wall?

  false
end

Protected Instance Methods

compute_lights() click to toggle source
# File lib/natural_20/battle_map.rb, line 802
def compute_lights
  @light_map = @light_builder.build_map
end
setup_npcs() click to toggle source
# File lib/natural_20/battle_map.rb, line 834
def setup_npcs
  @meta_map&.each_with_index do |meta_row, column_index|
    meta_row.each_with_index do |token, row_index|
      token_type = @legend.dig(token.to_sym, :type)

      case token_type
      when 'npc'
        npc_meta = @legend[token.to_sym]
        raise 'npc type requires sub_type as well' unless npc_meta[:sub_type]

        entity = session.npc(npc_meta[:sub_type].to_sym, name: npc_meta[:name], overrides: npc_meta[:overrides],
                                                         rand_life: true)

        add(entity, column_index, row_index, group: npc_meta[:group])
      when 'spawn_point'
        @spawn_points[@legend.dig(token.to_sym, :name)] = {
          location: [column_index, row_index]
        }
      end
    end
  end
end
setup_objects() click to toggle source
# File lib/natural_20/battle_map.rb, line 806
def setup_objects
  @size[0].times do |pos_x|
    @size[1].times do |pos_y|
      tokens = [@base_map_1[pos_x][pos_y], @base_map[pos_x][pos_y]].compact
      tokens.map do |token|
        case token
        when '#'
          object_info = @session.load_object('stone_wall')
          obj = ItemLibrary::StoneWall.new(self, object_info)
          @interactable_objects[obj] = [pos_x, pos_y]
          place_object(obj, pos_x, pos_y)
        when '?'
          nil
        when '.'
          place_object(ItemLibrary::Ground.new(self, name: 'ground'), pos_x, pos_y)
        else
          object_meta = @legend[token.to_sym]
          raise "unknown object token #{token}" if object_meta.nil?
          next nil if object_meta[:type] == 'mask' # this is a mask terrain so ignore

          object_info = @session.load_object(object_meta[:type])
          place_object(object_info, pos_x, pos_y, object_meta)
        end
      end
    end
  end
end