Class: Natural20::BattleMap
- Inherits:
-
Object
- Object
- Natural20::BattleMap
- Includes:
- Cover, MovementHelper
- Defined in:
- lib/natural_20/battle_map.rb
Instance Attribute Summary collapse
-
#area_triggers ⇒ Object
readonly
Returns the value of attribute area_triggers.
-
#base_map ⇒ Object
readonly
Returns the value of attribute base_map.
-
#entities ⇒ Object
readonly
Returns the value of attribute entities.
-
#feet_per_grid ⇒ Object
readonly
Returns the value of attribute feet_per_grid.
-
#interactable_objects ⇒ Object
readonly
Returns the value of attribute interactable_objects.
-
#properties ⇒ Object
readonly
Returns the value of attribute properties.
-
#session ⇒ Object
readonly
Returns the value of attribute session.
-
#size ⇒ Object
readonly
Returns the value of attribute size.
-
#spawn_points ⇒ Object
readonly
Returns the value of attribute spawn_points.
-
#tokens ⇒ Object
readonly
Returns the value of attribute tokens.
-
#unaware_npcs ⇒ Object
readonly
Returns the value of attribute unaware_npcs.
Instance Method Summary collapse
- #activate_map_triggers(trigger_type, source, opt = {}) ⇒ Object
- #add(entity, pos_x, pos_y, group: :b) ⇒ Object
- #area_trigger!(entity, position, is_flying) ⇒ Object
-
#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.
-
#can_see_square?(entity, pos2_x, pos2_y, allow_dark_vision: true) ⇒ Boolean
Test to see if an entity can see a square.
- #cover_at(pos_x, pos_y, entity = false) ⇒ Object
-
#difficult_terrain?(entity, pos_x, pos_y, _battle = nil) ⇒ Boolean
Determines if terrain is a difficult terrain.
-
#distance(entity1, entity2, entity_1_pos: nil, entity_2_pos: nil) ⇒ Object
Computes the distance between two entities.
-
#entity_at(pos_x, pos_y) ⇒ Natural20::Entity
Get entity at map location.
- #entity_or_object_pos(thing) ⇒ Array<Integer,Integer>
-
#entity_squares(entity, squeeze = false) ⇒ Array
Get all the location of the squares occupied by said entity (e.g. for large, huge creatures).
-
#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.
- #ground_at(pos_x, pos_y) ⇒ Object
-
#highlight(source, perception_check) ⇒ Object
highlights objects of interest if enabled on object.
-
#initialize(session, map_file) ⇒ BattleMap
constructor
A new instance of BattleMap.
- #items_on_the_ground(entity) ⇒ Object
- #jump_required?(entity, pos_x, pos_y) ⇒ Boolean
- #light_at(pos_x, pos_y) ⇒ Object
-
#line_distance(entity1, pos2_x, pos2_y, entity_1_pos: nil) ⇒ Object
Returns the line distance between an entity and a location.
-
#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.
-
#line_of_sight_for?(entity, pos2_x, pos2_y, distance = nil) ⇒ TrueClass, FalseClass
Compute if entity is in line of sight.
-
#look(entity, distance = nil) ⇒ Hash
Natural20::Entity to look around.
-
#move_to!(entity, pos_x, pos_y, battle) ⇒ Object
Moves an entity to a specified location on the board.
- #movement_cost(entity, path, battle = nil, manual_jump = []) ⇒ Natural20::MovementHelper::Movement
-
#object_at(pos_x, pos_y, reveal_concealed: false) ⇒ ItemLibrary::Object
Get object at map location.
-
#objects_at(pos_x, pos_y) ⇒ Array<ItemLibrary::Object>
Get object at map location.
-
#objects_near(entity, battle = nil) ⇒ Array
Lists interactable objects near an entity.
-
#opaque?(pos_x, pos_y) ⇒ Boolean
check if this interrupts line of sight (not necessarily movement).
-
#passable?(entity, pos_x, pos_y, battle = nil, allow_squeeze = true) ⇒ Boolean
Describes if terrain is passable or not.
-
#perception_on(entity, source, perception_check) ⇒ Array
Reads perception related notes on an entity.
-
#perception_on_area(pos_x, pos_y, source, perception_check) ⇒ Array
Reads perception related notes on an area.
-
#place(pos_x, pos_y, entity, token = nil, battle = nil) ⇒ Object
Place token here if it is not already present on the board.
- #place_at_spawn_point(position, entity, token = nil, battle = nil) ⇒ Object
-
#place_object(object_info, pos_x, pos_y, object_meta = {}) ⇒ ItemLibrary::Object
Places an object onto the map.
-
#placeable?(entity, pos_x, pos_y, battle = nil, squeeze = true) ⇒ Boolean
Determines if it is possible to place a token in this location.
- #position_of(entity) ⇒ Object
- #squares_in_path(pos1_x, pos1_y, pos2_x, pos2_y, distance: nil, inclusive: true) ⇒ Object
-
#thing_at(pos_x, pos_y, reveal_concealed: false) ⇒ Array<Nautral20::Entity>
Get entity or object at map location.
- #valid_position?(pos_x, pos_y) ⇒ Boolean
- #wall?(pos_x, pos_y) ⇒ Boolean
Methods included from MovementHelper
#compute_actual_moves, #requires_squeeze?, #retrieve_opportunity_attacks, #valid_move_path?
Methods included from Cover
Constructor Details
#initialize(session, map_file) ⇒ BattleMap
Returns a new instance of BattleMap.
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) = @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_triggers ⇒ Object (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_map ⇒ Object (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 |
#entities ⇒ Object (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_grid ⇒ Object (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_objects ⇒ Object (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 |
#properties ⇒ Object (readonly)
Returns the value of attribute properties.
7 8 9 |
# File 'lib/natural_20/battle_map.rb', line 7 def properties @properties end |
#session ⇒ Object (readonly)
Returns the value of attribute session.
7 8 9 |
# File 'lib/natural_20/battle_map.rb', line 7 def session @session end |
#size ⇒ Object (readonly)
Returns the value of attribute size.
7 8 9 |
# File 'lib/natural_20/battle_map.rb', line 7 def size @size end |
#spawn_points ⇒ Object (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 |
#tokens ⇒ Object (readonly)
Returns the value of attribute tokens.
7 8 9 |
# File 'lib/natural_20/battle_map.rb', line 7 def tokens @tokens end |
#unaware_npcs ⇒ Object (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]&.(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
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, ) trigger_results = @area_triggers.map do |k, _prop| next if k.dead? k.area_trigger_handler(entity, position, ) 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
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
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
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
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
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
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>
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)
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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, = {}) 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()) @area_triggers[item_obj] = {} if item_klass.included_modules.include?(ItemLibrary::AreaTrigger) item_obj else ItemLibrary::Object.new(self, .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
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
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
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
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 |