Class: Natural20::Battle
- Inherits:
-
Object
- Object
- Natural20::Battle
- Defined in:
- lib/natural_20/battle.rb
Instance Attribute Summary collapse
-
#battle_log ⇒ Object
readonly
Returns the value of attribute battle_log.
-
#combat_order ⇒ Object
Returns the value of attribute combat_order.
-
#current_party ⇒ Object
Returns the value of attribute current_party.
-
#entities ⇒ Object
readonly
Returns the value of attribute entities.
-
#in_combat ⇒ Object
readonly
Returns the value of attribute in_combat.
-
#map ⇒ Object
readonly
Returns the value of attribute map.
-
#round ⇒ Object
Returns the value of attribute round.
-
#session ⇒ Object
readonly
Returns the value of attribute session.
-
#started ⇒ Object
readonly
Returns the value of attribute started.
Instance Method Summary collapse
- #action(source, action_type, opts = {}) ⇒ Object
- #action!(action) ⇒ Object
- #active_perception_for(entity) ⇒ Object
-
#add(entity, group, controller: nil, position: nil, token: nil) ⇒ Object
Adds an entity to the battle.
- #add_battlefield_event_listener(event, object, handler) ⇒ Object
-
#allies?(entity1, entity2) ⇒ Boolean
Determines if two entities are allies of each other.
-
#ally_within_enemey_melee_range?(source, target, exclude = [], source_pos: nil) ⇒ Boolean
Determines if there is a conscious ally within melee range of target.
- #battle_ends? ⇒ Boolean
-
#can_see?(entity1, entity2, active_perception: 0, entity_1_pos: nil, entity_2_pos: nil) ⇒ Boolean
Determines if an entity can see someone in battle.
- #check_combat ⇒ Object
- #combat? ⇒ Boolean
- #commit(action) ⇒ Object
- #compute_max_weapon_range(action, range = nil) ⇒ Object
-
#consume(entity, resource, qty = 1) ⇒ Object
Consumes an action resource.
-
#consume!(entity, resource, qty = 1) ⇒ Object
consume action resource and return if something changed.
- #controller_for(entity) ⇒ Object
- #current_turn ⇒ Natural20::Entity
- #dismiss_help_actions_for(source) ⇒ Object
- #dismiss_help_for(target) ⇒ Object
-
#enemy_in_melee_range?(source, exclude = [], source_pos: nil) ⇒ Boolean
Determines if there is a conscious enemey within melee range.
- #entity_group_for(entity) ⇒ Object
- #entity_state_for(entity) ⇒ Hash
- #first_hand_weapon(entity) ⇒ Object
-
#has_controller_for?(entity) ⇒ Boolean
Checks if this entity is controlled by AI or Person.
- #help_with?(target) ⇒ Boolean
- #in_battle?(entity) ⇒ Boolean
-
#initialize(session, map, standard_controller = nil) ⇒ Battle
constructor
Create an instance of a battle.
- #move_for(entity) ⇒ Object
- #ongoing? ⇒ Boolean
-
#opponents_of?(entity) ⇒ Array<Natrual20::Entity>
Retruns opponents of entity.
-
#opposing?(entity1, entity2) ⇒ Boolean
Determines if two entities are opponents of each other.
-
#register_players(party, controller) ⇒ Object
Registers a player party.
- #roll_for(entity, die_type, number_of_times, description, advantage: false, disadvantage: false) ⇒ Object
- #start(combat_order = nil) ⇒ Object
- #tpk? ⇒ Boolean
- #trigger_event!(event, source, opt = {}) ⇒ Object
- #trigger_opportunity_attack(entity, target, cur_x, cur_y) ⇒ Object
- #two_weapon_attack?(entity) ⇒ Boolean
-
#update_group_dynamics(opposing_groups) ⇒ Object
Updates opposing player groups to determine who is the enemy of who.
-
#valid_targets_for(entity, action, target_types: [:enemies], range: nil, active_perception: nil, include_objects: false, filter: nil) ⇒ Natural20::Entity
Generates targets that make sense for a given action.
- #while_active(max_rounds = nil, &block) ⇒ Object
Constructor Details
#initialize(session, map, standard_controller = nil) ⇒ Battle
Create an instance of a battle
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/natural_20/battle.rb', line 11 def initialize(session, map, standard_controller = nil) @session = session @entities = {} @groups = {} @battle_field_events = {} @battle_log = [] @combat_order = [] @late_comers = [] @current_turn_index = 0 @round = 0 @map = map @in_combat = false @standard_controller = standard_controller @opposing_groups = { a: [:b], b: [:a] } standard_controller&.register_battle_listeners(self) end |
Instance Attribute Details
#battle_log ⇒ Object (readonly)
Returns the value of attribute battle_log.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def battle_log @battle_log end |
#combat_order ⇒ Object
Returns the value of attribute combat_order.
4 5 6 |
# File 'lib/natural_20/battle.rb', line 4 def combat_order @combat_order end |
#current_party ⇒ Object
Returns the value of attribute current_party.
4 5 6 |
# File 'lib/natural_20/battle.rb', line 4 def current_party @current_party end |
#entities ⇒ Object (readonly)
Returns the value of attribute entities.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def entities @entities end |
#in_combat ⇒ Object (readonly)
Returns the value of attribute in_combat.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def in_combat @in_combat end |
#map ⇒ Object (readonly)
Returns the value of attribute map.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def map @map end |
#round ⇒ Object
Returns the value of attribute round.
4 5 6 |
# File 'lib/natural_20/battle.rb', line 4 def round @round end |
#session ⇒ Object (readonly)
Returns the value of attribute session.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def session @session end |
#started ⇒ Object (readonly)
Returns the value of attribute started.
5 6 7 |
# File 'lib/natural_20/battle.rb', line 5 def started @started end |
Instance Method Details
#action(source, action_type, opts = {}) ⇒ Object
185 186 187 188 189 190 191 |
# File 'lib/natural_20/battle.rb', line 185 def action(source, action_type, opts = {}) action = source.available_actions(@session, self).detect { |act| act.action_type == action_type } opts[:battle] = self return action.resolve(@session, @map, opts) if action nil end |
#action!(action) ⇒ Object
193 194 195 196 197 198 |
# File 'lib/natural_20/battle.rb', line 193 def action!(action) opts = { battle: self } action.resolve(@session, @map, opts) end |
#active_perception_for(entity) ⇒ Object
220 221 222 |
# File 'lib/natural_20/battle.rb', line 220 def active_perception_for(entity) @entities[entity][:active_perception] || 0 end |
#add(entity, group, controller: nil, position: nil, token: nil) ⇒ Object
Adds an entity to the battle
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/natural_20/battle.rb', line 71 def add(entity, group, controller: nil, position: nil, token: nil) return if @entities[entity] raise 'entity cannot be nil' if entity.nil? state = { group: group, action: 0, bonus_action: 0, reaction: 0, movement: 0, stealth: 0, statuses: Set.new, active_perception: 0, active_perception_disadvantage: 0, free_object_interaction: 0, target_effect: {}, two_weapon: nil, controller: controller || @standard_controller } @entities[entity] = state battle_defaults = entity.try(:battle_defaults) if battle_defaults battle_defaults[:statuses].each { |s| state[:statuses].add(s.to_sym) } unless state[:stealth].blank? state[:stealth] = DieRoll.roll(battle_defaults[:stealth], description: t('dice_roll.stealth'), entity: entity, battle: self).result end end @groups[group] ||= Set.new @groups[group].add(entity) # battle already ongoing... if started @late_comers << entity @entities[entity][:initiative] = entity.initiative!(self) end return if position.nil? return if @map.nil? if position.is_a?(Array) @map.place(*position, entity, token, self) else @map.place_at_spawn_point(position, entity, token) end end |
#add_battlefield_event_listener(event, object, handler) ⇒ Object
52 53 54 55 |
# File 'lib/natural_20/battle.rb', line 52 def add_battlefield_event_listener(event, object, handler) @battle_field_events[event.to_sym] ||= [] @battle_field_events[event.to_sym] << [object, handler] end |
#allies?(entity1, entity2) ⇒ Boolean
Determines if two entities are allies of each other
319 320 321 322 323 324 325 326 327 328 |
# File 'lib/natural_20/battle.rb', line 319 def allies?(entity1, entity2) source_state1 = entity_state_for(entity1) source_state2 = entity_state_for(entity2) return false if source_state1.nil? || source_state2.nil? source_group1 = source_state1[:group] source_group2 = source_state2[:group] source_group1 == source_group2 end |
#ally_within_enemey_melee_range?(source, target, exclude = [], source_pos: nil) ⇒ Boolean
Determines if there is a conscious ally within melee range of target
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/natural_20/battle.rb', line 448 def ally_within_enemey_melee_range?(source, target, exclude = [], source_pos: nil) objects_around_me = map.look(target) objects_around_me.detect do |object, _| next if exclude.include?(object) next if object == source state = entity_state_for(object) next unless state next unless object.conscious? return true if allies?(source, object) && (map.distance(target, object, entity_1_pos: source_pos) <= (object.melee_distance / map.feet_per_grid)) end false end |
#battle_ends? ⇒ Boolean
478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'lib/natural_20/battle.rb', line 478 def battle_ends? groups_present = @entities.keys.reject do |a| a.dead? || a.unconscious? end.map { |e| @entities[e][:group] }.uniq groups_present.each do |g| groups_present.each do |h| next if g == h raise "warning unknown group #{g}" unless @opposing_groups[g] return false if @opposing_groups[g].include?(h) end end true end |
#can_see?(entity1, entity2, active_perception: 0, entity_1_pos: nil, entity_2_pos: nil) ⇒ Boolean
Determines if an entity can see someone in battle
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/natural_20/battle.rb', line 275 def can_see?(entity1, entity2, active_perception: 0, entity_1_pos: nil, entity_2_pos: nil) return true if entity1 == entity2 return false unless @map.can_see?(entity1, entity2, entity_1_pos: entity_1_pos, entity_2_pos: entity_2_pos) return true unless entity2.hiding?(self) cover_value = @map.cover_calculation(@map, entity1, entity2, entity_1_pos: entity_1_pos, naturally_stealthy: entity2.class_feature?('naturally_stealthy')) if cover_value.positive? entity_2_state = entity_state_for(entity2) return false if entity_2_state[:stealth] > [active_perception, entity1.passive_perception].max end true end |
#check_combat ⇒ Object
374 375 376 377 378 379 380 381 382 |
# File 'lib/natural_20/battle.rb', line 374 def check_combat if !@started && !battle_ends? start Natural20::EventManager.received_event(source: self, event: :start_of_combat, target: current_turn, combat_order: @combat_order.map { |e| [e, @entities[e][:initiative]] }) return true end false end |
#combat? ⇒ Boolean
470 471 472 |
# File 'lib/natural_20/battle.rb', line 470 def combat? ongoing? end |
#commit(action) ⇒ Object
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
# File 'lib/natural_20/battle.rb', line 504 def commit(action) return if action.nil? # check_action_serialization(action) action.result.each do |item| Natural20::Action.descendants.each do |klass| klass.apply!(self, item) end end case action.action_type when :move trigger_event!(:movement, action.source, move_path: action.move_path) when :interact trigger_event!(:interact, action) end @battle_log << action end |
#compute_max_weapon_range(action, range = nil) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/natural_20/battle.rb', line 200 def compute_max_weapon_range(action, range = nil) case action.action_type when :help 5 when :attack if action.npc_action action.npc_action[:range_max].presence || action.npc_action[:range] elsif action.using weapon = session.load_weapon(action.using) if action.thrown weapon.dig(:thrown, :range_max) || weapon.dig(:thrown, :range) || weapon[:range] else weapon[:range_max].presence || weapon[:range] end end else range end end |
#consume(entity, resource, qty = 1) ⇒ Object
Consumes an action resource
533 534 535 536 537 538 |
# File 'lib/natural_20/battle.rb', line 533 def consume(entity, resource, qty = 1) raise 'invalid resource' unless i[action reaction bonus_action movement].include?(resource.to_sym) return unless entity_state_for(entity) entity_state_for(entity)[resource.to_sym] = [0, entity_state_for(entity)[resource.to_sym] - qty].max end |
#consume!(entity, resource, qty = 1) ⇒ Object
consume action resource and return if something changed
340 341 342 343 344 345 346 |
# File 'lib/natural_20/battle.rb', line 340 def consume!(entity, resource, qty = 1) current_qty = entity_state_for(entity)[resource.to_sym] new_qty = [0, current_qty - qty].max entity_state_for(entity)[resource.to_sym] = new_qty current_qty != new_qty end |
#controller_for(entity) ⇒ Object
132 133 134 135 136 |
# File 'lib/natural_20/battle.rb', line 132 def controller_for(entity) return nil unless @entities.key? entity @entities[entity][:controller] end |
#current_turn ⇒ Natural20::Entity
370 371 372 |
# File 'lib/natural_20/battle.rb', line 370 def current_turn @combat_order[@current_turn_index] end |
#dismiss_help_actions_for(source) ⇒ Object
167 168 169 170 171 |
# File 'lib/natural_20/battle.rb', line 167 def dismiss_help_actions_for(source) @entities.each do |_k, entity| entity[:target_effect]&.delete(source) if i[help help_ability_check].include?(entity[:target_effect][source]) end end |
#dismiss_help_for(target) ⇒ Object
179 180 181 182 183 |
# File 'lib/natural_20/battle.rb', line 179 def dismiss_help_for(target) return unless @entities[target] @entities[target][:target_effect].delete_if { |_k, v| v == :help } end |
#enemy_in_melee_range?(source, exclude = [], source_pos: nil) ⇒ Boolean
Determines if there is a conscious enemey within melee range
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/natural_20/battle.rb', line 428 def enemy_in_melee_range?(source, exclude = [], source_pos: nil) objects_around_me = map.look(source) objects_around_me.detect do |object, _| next if exclude.include?(object) state = entity_state_for(object) next unless state next unless object.conscious? return true if opposing?(source, object) && (map.distance(source, object, entity_1_pos: source_pos) <= (object.melee_distance / map.feet_per_grid)) end false end |
#entity_group_for(entity) ⇒ Object
161 162 163 164 165 |
# File 'lib/natural_20/battle.rb', line 161 def entity_group_for(entity) return :none unless @entities[entity] @entities[entity][:group] end |
#entity_state_for(entity) ⇒ Hash
157 158 159 |
# File 'lib/natural_20/battle.rb', line 157 def entity_state_for(entity) @entities[entity] end |
#first_hand_weapon(entity) ⇒ Object
61 62 63 |
# File 'lib/natural_20/battle.rb', line 61 def first_hand_weapon(entity) entity_state_for(entity)[:two_weapon] end |
#has_controller_for?(entity) ⇒ Boolean
Checks if this entity is controlled by AI or Person
333 334 335 336 337 |
# File 'lib/natural_20/battle.rb', line 333 def has_controller_for?(entity) raise 'unknown entity in battle' unless @entities.key?(entity) @entities[entity][:controller] != :manual end |
#help_with?(target) ⇒ Boolean
173 174 175 176 177 |
# File 'lib/natural_20/battle.rb', line 173 def help_with?(target) return @entities[target][:target_effect].values.include?(:help) if @entities[target] false end |
#in_battle?(entity) ⇒ Boolean
124 125 126 |
# File 'lib/natural_20/battle.rb', line 124 def in_battle?(entity) @entities.key?(entity) end |
#move_for(entity) ⇒ Object
128 129 130 |
# File 'lib/natural_20/battle.rb', line 128 def move_for(entity) @entities[entity][:controller].move_for(entity, self) end |
#ongoing? ⇒ Boolean
466 467 468 |
# File 'lib/natural_20/battle.rb', line 466 def ongoing? @started end |
#opponents_of?(entity) ⇒ Array<Natrual20::Entity>
Retruns opponents of entity
294 295 296 297 298 |
# File 'lib/natural_20/battle.rb', line 294 def opponents_of?(entity) (@entities.keys + @late_comers).reject(&:dead?).select do |k| opposing?(k, entity) end end |
#opposing?(entity1, entity2) ⇒ Boolean
Determines if two entities are opponents of each other
304 305 306 307 308 309 310 311 312 313 |
# File 'lib/natural_20/battle.rb', line 304 def opposing?(entity1, entity2) source_state1 = entity_state_for(entity1) source_state2 = entity_state_for(entity2) return false if source_state1.nil? || source_state2.nil? source_group1 = source_state1[:group] source_group2 = source_state2[:group] @opposing_groups[source_group1]&.include?(source_group2) end |
#register_players(party, controller) ⇒ Object
Registers a player party
36 37 38 39 40 41 42 43 44 |
# File 'lib/natural_20/battle.rb', line 36 def register_players(party, controller) return if party.blank? party.each_with_index do |pc, index| add(pc, :a, position: "spawn_point_#{index + 1}", controller: controller) end @current_party = party @combat_order = party end |
#roll_for(entity, die_type, number_of_times, description, advantage: false, disadvantage: false) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/natural_20/battle.rb', line 138 def roll_for(entity, die_type, number_of_times, description, advantage: false, disadvantage: false) controller = if @entities[entity] && @entities[entity][:controller] @entities[entity][:controller] else @standard_controller end rolls = controller.try(:roll_for, entity, die_type, number_of_times, description, advantage: advantage, disadvantage: disadvantage) return rolls if rolls if advantage || disadvantage number_of_times.times.map { [(1..die_type).to_a.sample, (1..die_type).to_a.sample] } else number_of_times.times.map { (1..die_type).to_a.sample } end end |
#start(combat_order = nil) ⇒ Object
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/natural_20/battle.rb', line 349 def start(combat_order = nil) if combat_order @combat_order = combat_order return end # roll for initiative @combat_order = @entities.map do |entity, v| next if entity.dead? v[:initiative] = entity.initiative!(self) entity end.compact @started = true @current_turn_index = 0 @combat_order = @combat_order.sort_by { |a| @entities[a][:initiative] || a.name }.reverse end |
#tpk? ⇒ Boolean
474 475 476 |
# File 'lib/natural_20/battle.rb', line 474 def tpk? @current_party && !@current_party.detect(&:conscious?) end |
#trigger_event!(event, source, opt = {}) ⇒ Object
523 524 525 526 527 528 |
# File 'lib/natural_20/battle.rb', line 523 def trigger_event!(event, source, opt = {}) @battle_field_events[event.to_sym]&.each do |object, handler| object.send(handler, self, source, opt.merge(ui_controller: controller_for(source))) end @map.activate_map_triggers(event, source, opt.merge(ui_controller: controller_for(source))) end |
#trigger_opportunity_attack(entity, target, cur_x, cur_y) ⇒ Object
492 493 494 495 496 497 498 499 500 501 502 |
# File 'lib/natural_20/battle.rb', line 492 def trigger_opportunity_attack(entity, target, cur_x, cur_y) event = { target: target, position: [cur_x, cur_y] } action = entity.trigger_event(:opportunity_attack, self, @session, @map, event) if action action.resolve(@session, @map, battle: self) commit(action) end end |
#two_weapon_attack?(entity) ⇒ Boolean
57 58 59 |
# File 'lib/natural_20/battle.rb', line 57 def two_weapon_attack?(entity) !!entity_state_for(entity)[:two_weapon] end |
#update_group_dynamics(opposing_groups) ⇒ Object
Updates opposing player groups to determine who is the enemy of who
48 49 50 |
# File 'lib/natural_20/battle.rb', line 48 def update_group_dynamics(opposing_groups) @opposing_groups = opposing_groups end |
#valid_targets_for(entity, action, target_types: [:enemies], range: nil, active_perception: nil, include_objects: false, filter: nil) ⇒ Natural20::Entity
Generates targets that make sense for a given action
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/natural_20/battle.rb', line 230 def valid_targets_for(entity, action, target_types: [:enemies], range: nil, active_perception: nil, include_objects: false, filter: nil) raise 'not an action' unless action.is_a?(Natural20::Action) active_perception = active_perception.nil? ? active_perception_for(entity) : 0 target_types = target_types&.map(&:to_sym) || [:enemies] entity_group = @entities[entity][:group] attack_range = compute_max_weapon_range(action, range) raise 'attack range cannot be nil' if attack_range.nil? targets = @entities.map do |k, prop| next if !target_types.include?(:self) && k == entity next if !target_types.include?(:allies) && prop[:group] == entity_group && k != entity next if !target_types.include?(:enemies) && opposing?(entity, k) next if k.dead? next if k.hp.nil? next if !target_types.include?(:ignore_los) && !can_see?(entity, k, active_perception: active_perception) next if @map.distance(k, entity) * @map.feet_per_grid > attack_range next if filter && !k.eval_if(filter) action.target = k action.validate next unless action.errors.empty? k end.compact if include_objects targets += @map.interactable_objects.map do |object, _position| next if object.dead? next if !target_types.include?(:ignore_los) && !can_see?(entity, object, active_perception: active_perception) next if @map.distance(object, entity) * @map.feet_per_grid > attack_range next if filter && !k.eval_if(filter) object end.compact end targets end |
#while_active(max_rounds = nil, &block) ⇒ Object
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/natural_20/battle.rb', line 384 def while_active(max_rounds = nil, &block) loop do Natural20::EventManager.received_event(source: self, event: :start_of_round, in_battle: ongoing?) current_turn.death_saving_throw!(self) if current_turn.unconscious? && !current_turn.stable? if current_turn.conscious? current_turn.reset_turn!(self) next if block.call(current_turn) end return :tpk if tpk? trigger_event!(:end_of_round, self, target: current_turn) if @started && battle_ends? Natural20::EventManager.received_event(source: self, event: :end_of_combat) @started = false end @current_turn_index += 1 next unless @current_turn_index >= @combat_order.length @current_turn_index = 0 @round += 1 # top of the round unless @late_comers.empty? @combat_order += @late_comers @late_comers.clear @combat_order = @combat_order.sort_by { |a| @entities[a][:initiative] || a.name }.reverse end session.increment_game_time! Natural20::EventManager.received_event({ source: self, event: :top_of_the_round, round: @round, target: current_turn }) return if !max_rounds.nil? && @round > max_rounds end end |