Class: Natural20::BattleMap

Inherits:
Object
  • Object
show all
Includes:
Cover, MovementHelper
Defined in:
lib/natural_20/battle_map.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MovementHelper

#compute_actual_moves, #requires_squeeze?, #retrieve_opportunity_attacks, #valid_move_path?

Methods included from Cover

#cover_calculation

Constructor Details

#initialize(session, map_file) ⇒ BattleMap

Returns a new instance of BattleMap.

Parameters:

  • session (Natural20::Session)

    The current game session

  • map_file (String)

    Path to map file



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# 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

Instance Attribute Details

#area_triggersObject (readonly)

Returns the value of attribute area_triggers.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def area_triggers
  @area_triggers
end

#base_mapObject (readonly)

Returns the value of attribute base_map.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def base_map
  @base_map
end

#entitiesObject (readonly)

Returns the value of attribute entities.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def entities
  @entities
end

#feet_per_gridObject (readonly)

Returns the value of attribute feet_per_grid.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def feet_per_grid
  @feet_per_grid
end

#interactable_objectsObject (readonly)

Returns the value of attribute interactable_objects.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def interactable_objects
  @interactable_objects
end

#propertiesObject (readonly)

Returns the value of attribute properties.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def properties
  @properties
end

#sessionObject (readonly)

Returns the value of attribute session.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def session
  @session
end

#sizeObject (readonly)

Returns the value of attribute size.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def size
  @size
end

#spawn_pointsObject (readonly)

Returns the value of attribute spawn_points.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def spawn_points
  @spawn_points
end

#tokensObject (readonly)

Returns the value of attribute tokens.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def tokens
  @tokens
end

#unaware_npcsObject (readonly)

Returns the value of attribute unaware_npcs.



7
8
9
# File 'lib/natural_20/battle_map.rb', line 7

def unaware_npcs
  @unaware_npcs
end

Instance Method Details

#activate_map_triggers(trigger_type, source, opt = {}) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 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) ⇒ Object

Parameters:

  • entity (Natural20::Battle)
  • pos_x (Integer)
  • pos_y (Integer)
  • group (Symbol) (defaults to: :b)


794
795
796
797
798
# 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) ⇒ Object



735
736
737
738
739
740
741
742
743
# 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) ⇒ Boolean

Checks if an entity can see another

Parameters:

  • entity (Natural20::Entity)

    entity looking

  • entity2 (Natural20::Entity)

    entity being looked at

  • distance (Integer) (defaults to: nil)
  • entity_2_pos (Array) (defaults to: nil)

    position override for entity2

  • allow_dark_vision (Boolean) (defaults to: true)

    Allow darkvision

Returns:

  • (Boolean)


372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# 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) ⇒ Boolean

Test to see if an entity can see a square

Parameters:

  • entity (Natural20::Entity)
  • pos2_x (Integer)
  • pos2_y (Integer)
  • allow_dark_vision (Boolean) (defaults to: true)

    Allow darkvision

Returns:

  • (Boolean)


341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# 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) ⇒ Object

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)
  • entity (Boolean) (defaults to: false)

    inlcude entities



668
669
670
671
672
673
674
675
676
# 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) ⇒ Boolean

Determines if terrain is a difficult terrain

Parameters:

Returns:

  • (Boolean)

    Returns if difficult terrain or not



614
615
616
617
618
619
620
621
622
623
# 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) ⇒ Object

Computes the distance between two entities

Parameters:



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# 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) ⇒ Natural20::Entity

Get entity at map location

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)

Returns:



420
421
422
423
424
425
# 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) ⇒ Array<Integer,Integer>

Parameters:

Returns:

  • (Array<Integer,Integer>)


747
748
749
# 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) ⇒ Array

Get all the location of the squares occupied by said entity (e.g. for large, huge creatures)

Parameters:

Returns:

  • (Array)


265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# 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) ⇒ Array

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

Parameters:

Returns:

  • (Array)


291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# 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) ⇒ Object



185
186
187
188
# 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) ⇒ Object

highlights objects of interest if enabled on object

Parameters:

  • source (Natural20::Entity)

    entity that is observing

  • perception_check (Integer)

    Perception value



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# 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) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# 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) ⇒ Boolean

Returns:

  • (Boolean)


625
626
627
628
629
630
631
632
633
# 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) ⇒ Object



404
405
406
407
408
409
410
# 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) ⇒ Object

Returns the line distance between an entity and a location

Parameters:



253
254
255
256
257
258
259
260
# 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) ⇒ Array<Array<Integer,Integer>>

Computes one of sight between two points

Parameters:

  • pos1_x (Integer)
  • pos1_y (Integer)
  • pos2_x (Integer)
  • pos2_y (Integer)
  • distance (Integer) (defaults to: nil)

Returns:

  • (Array<Array<Integer,Integer>>)

    Cover characteristics if there is LOS



654
655
656
657
658
659
660
661
662
663
# 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) ⇒ TrueClass, FalseClass

Compute if entity is in line of sight

Parameters:

  • entity (Natural20::Entity)
  • pos2_x (Integer)
  • pos2_y (Integer)
  • distance (Integer) (defaults to: nil)

Returns:

  • (TrueClass, FalseClass)


328
329
330
331
332
333
# 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) ⇒ Hash

Natural20::Entity to look around

Parameters:

Returns:

  • (Hash)

    entities in line of sight



311
312
313
314
315
316
317
318
319
320
# 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) ⇒ Object

Moves an entity to a specified location on the board

Parameters:



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# 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 = []) ⇒ Natural20::MovementHelper::Movement

Parameters:

Returns:



492
493
494
495
496
497
498
# 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) ⇒ ItemLibrary::Object

Get object at map location

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)
  • reveal_concealed (Boolean) (defaults to: false)

Returns:



131
132
133
# 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) ⇒ Array<ItemLibrary::Object>

Get object at map location

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)

Returns:



139
140
141
# 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) ⇒ Array

Lists interactable objects near an entity

Parameters:

Returns:

  • (Array)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


636
637
638
639
640
641
642
643
644
645
# 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) ⇒ Boolean

Describes if terrain is passable or not

Parameters:

  • entity (Natural20::Entity)
  • pos_x (Integer)
  • pos_y (Integer)
  • battle (Natural20::Battle) (defaults to: nil)
  • allow_squeeze (Boolean) (defaults to: true)

    Allow entity to squeeze inside a space (PHB )

Returns:

  • (Boolean)


507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# 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) ⇒ Array

Reads perception related notes on an entity

Parameters:

  • entity (Object)

    The target to percive on

  • source (Natural20::Entity)

    Entity perceiving

  • perception_check (Integer)

    Perception roll of source

Returns:

  • (Array)

    Array of notes



703
704
705
706
707
708
# 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) ⇒ Array

Reads perception related notes on an area

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)
  • source (Natural20::Entity)
  • perception_check (Integer)

    Perception roll of source

Returns:

  • (Array)

    Array of notes



716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# 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) ⇒ Object

Place token here if it is not already present on the board

Parameters:



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 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) ⇒ Object



217
218
219
220
221
222
223
224
225
# 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 = {}) ⇒ ItemLibrary::Object

Places an object onto the map

Parameters:

  • object_info (Hash)
  • pos_x (Integer)
  • pos_y (Integer)
  • object_meta (Hash) (defaults to: {})

Returns:



757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# 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) ⇒ Boolean

Determines if it is possible to place a token in this location

Parameters:

Returns:

  • (Boolean)


594
595
596
597
598
599
600
601
602
603
604
605
606
# 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) ⇒ Object



412
413
414
# 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) ⇒ Object



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
# 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) ⇒ Array<Nautral20::Entity>

Get entity or object at map location

Parameters:

  • pos_x (Integer)
  • pos_y (Integer)
  • reveal_concealed (Boolean) (defaults to: false)

    Included concealed objects

Returns:

  • (Array<Nautral20::Entity>)


432
433
434
435
436
437
# 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) ⇒ Boolean

Returns:

  • (Boolean)


478
479
480
481
482
483
484
485
# 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) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
# 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