natural_20 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +13 -0
  4. data/README.md +1 -0
  5. data/bin/nat20 +2 -1
  6. data/bin/nat20.cmd +0 -0
  7. data/char_classes/wizard.yml +89 -0
  8. data/characters/high_elf_mage.yml +27 -0
  9. data/fixtures/battle_sim_objects.yml +2 -2
  10. data/fixtures/high_elf_mage.yml +28 -0
  11. data/fixtures/large_map.yml +63 -0
  12. data/game.yml +2 -2
  13. data/items/equipment.yml +30 -0
  14. data/items/objects.yml +33 -29
  15. data/items/spells.yml +58 -0
  16. data/items/weapons.yml +78 -18
  17. data/lib/CHANGELOG.md +0 -0
  18. data/lib/natural_20.rb +9 -0
  19. data/lib/natural_20/actions/action.rb +2 -2
  20. data/lib/natural_20/actions/attack_action.rb +76 -67
  21. data/lib/natural_20/actions/concerns/action_damage.rb +3 -1
  22. data/lib/natural_20/actions/dash_action.rb +7 -10
  23. data/lib/natural_20/actions/disengage_action.rb +11 -12
  24. data/lib/natural_20/actions/dodge_action.rb +7 -8
  25. data/lib/natural_20/actions/escape_grapple_action.rb +16 -18
  26. data/lib/natural_20/actions/first_aid_action.rb +14 -16
  27. data/lib/natural_20/actions/grapple_action.rb +24 -28
  28. data/lib/natural_20/actions/ground_interact_action.rb +1 -3
  29. data/lib/natural_20/actions/help_action.rb +13 -16
  30. data/lib/natural_20/actions/hide_action.rb +7 -9
  31. data/lib/natural_20/actions/interact_action.rb +12 -14
  32. data/lib/natural_20/actions/look_action.rb +14 -15
  33. data/lib/natural_20/actions/move_action.rb +9 -9
  34. data/lib/natural_20/actions/multiattack_action.rb +8 -9
  35. data/lib/natural_20/actions/prone_action.rb +4 -6
  36. data/lib/natural_20/actions/short_rest_action.rb +7 -8
  37. data/lib/natural_20/actions/shove_action.rb +20 -24
  38. data/lib/natural_20/actions/spell_action.rb +89 -0
  39. data/lib/natural_20/actions/stand_action.rb +5 -7
  40. data/lib/natural_20/actions/use_item_action.rb +7 -9
  41. data/lib/natural_20/ai_controller/standard.rb +1 -1
  42. data/lib/natural_20/battle.rb +8 -3
  43. data/lib/natural_20/cli/action_ui.rb +180 -0
  44. data/lib/natural_20/cli/builder/fighter_builder.rb +1 -1
  45. data/lib/natural_20/cli/builder/rogue_builder.rb +10 -10
  46. data/lib/natural_20/cli/builder/wizard_builder.rb +77 -0
  47. data/lib/natural_20/cli/character_builder.rb +9 -4
  48. data/lib/natural_20/cli/commandline_ui.rb +55 -162
  49. data/lib/natural_20/cli/inventory_ui.rb +4 -0
  50. data/lib/natural_20/cli/map_renderer.rb +7 -1
  51. data/lib/natural_20/concerns/attack_helper.rb +53 -0
  52. data/lib/natural_20/concerns/entity.rb +170 -11
  53. data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +7 -9
  54. data/lib/natural_20/concerns/fighter_class.rb +2 -2
  55. data/lib/natural_20/concerns/spell_attack_helper.rb +33 -0
  56. data/lib/natural_20/concerns/wizard_class.rb +86 -0
  57. data/lib/natural_20/die_roll.rb +2 -2
  58. data/lib/natural_20/event_manager.rb +50 -44
  59. data/lib/natural_20/item_library/base_item.rb +1 -1
  60. data/lib/natural_20/npc.rb +4 -0
  61. data/lib/natural_20/player_character.rb +75 -12
  62. data/lib/natural_20/session.rb +14 -1
  63. data/lib/natural_20/spell_library/firebolt.rb +72 -0
  64. data/lib/natural_20/spell_library/mage_armor.rb +67 -0
  65. data/lib/natural_20/spell_library/mage_hand.rb +2 -0
  66. data/lib/natural_20/spell_library/magic_missile.rb +67 -0
  67. data/lib/natural_20/spell_library/shield.rb +69 -0
  68. data/lib/natural_20/spell_library/spell.rb +31 -0
  69. data/lib/natural_20/utils/weapons.rb +8 -6
  70. data/lib/natural_20/version.rb +1 -1
  71. data/locales/en.yml +44 -8
  72. data/maps/game_map.yml +12 -2
  73. metadata +22 -3
@@ -69,7 +69,7 @@ module Natural20
69
69
  end
70
70
 
71
71
  def t(key, options = {})
72
- I18n.t(key, options)
72
+ I18n.t(key, **options)
73
73
  end
74
74
  end
75
75
 
@@ -325,7 +325,7 @@ module Natural20
325
325
  end
326
326
 
327
327
  def self.t(key, options = {})
328
- I18n.t(key, options)
328
+ I18n.t(key, **options)
329
329
  end
330
330
  end
331
331
  end
@@ -53,10 +53,10 @@ module Natural20
53
53
  def self.standard_cli
54
54
  Natural20::EventManager.clear
55
55
  event_handlers = { died: lambda { |event|
56
- puts "#{show_name(event)} died."
56
+ output "#{show_name(event)} died."
57
57
  },
58
58
  unconscious: lambda { |event|
59
- puts "#{show_name(event)} unconscious."
59
+ output "#{show_name(event)} unconscious."
60
60
  },
61
61
  attacked: lambda { |event|
62
62
  advantage_mod = event[:advantage_mod]
@@ -68,7 +68,7 @@ module Natural20
68
68
  if event[:cover_ac].try(:positive?)
69
69
  cover_str = " (behind cover +#{event[:cover_ac]} ac)"
70
70
  end
71
-
71
+ Time.at(82800).utc.strftime("%I:%M%p")
72
72
  advantage_str = if advantage_mod&.positive?
73
73
  ' with advantage'.colorize(:green)
74
74
  elsif advantage_mod&.negative?
@@ -77,7 +77,7 @@ module Natural20
77
77
  ''
78
78
  end
79
79
  str_token = event[:attack_roll] ? 'event.attack' : 'event.attack_no_roll'
80
- puts t(str_token, opportunity: event[:as_reaction] ? 'Opportunity Attack: ' : '',
80
+ output t(str_token, opportunity: event[:as_reaction] ? 'Opportunity Attack: ' : '',
81
81
  source: show_name(event),
82
82
  target: "#{event[:target].name}#{cover_str}",
83
83
  attack_name: event[:attack_name],
@@ -87,126 +87,128 @@ module Natural20
87
87
  damage: damage_str)
88
88
  },
89
89
  damage: lambda { |event|
90
- puts "#{show_name(event)} #{event[:source].describe_health}"
90
+ output "#{show_name(event)} #{event[:source].describe_health}"
91
91
  },
92
92
  miss: lambda { |event|
93
93
  advantage_mod = event[:advantage_mod]
94
- advantage_str = if advantage_mod.positive?
94
+ advantage_str = if advantage_mod&.positive?
95
95
  ' with advantage'.colorize(:green)
96
- elsif advantage_mod.negative?
96
+ elsif advantage_mod&.negative?
97
97
  ' with disadvantage'.colorize(:red)
98
98
  else
99
99
  ''
100
100
  end
101
- puts "#{event[:as_reaction] ? 'Opportunity Attack: ' : ''} rolled #{advantage_str} #{event[:attack_roll]} ... #{event[:source].name&.colorize(:blue)} missed his attack #{event[:attack_name].colorize(:red)} on #{event[:target].name.colorize(:green)}"
101
+ output "#{event[:as_reaction] ? 'Opportunity Attack: ' : ''} rolled #{advantage_str} #{event[:attack_roll]} ... #{event[:source].name&.colorize(:blue)} missed his attack #{event[:attack_name].colorize(:red)} on #{event[:target].name.colorize(:green)}"
102
102
  },
103
103
  initiative: lambda { |event|
104
- puts "#{show_name(event)} rolled a #{event[:roll]} = (#{event[:value]}) with dex tie break for initiative."
104
+ output "#{show_name(event)} rolled a #{event[:roll]} = (#{event[:value]}) with dex tie break for initiative."
105
105
  },
106
106
  move: lambda { |event|
107
- puts "#{show_name(event)} moved #{(event[:path].size - 1) * event[:feet_per_grid]}ft."
107
+ output "#{show_name(event)} moved #{(event[:path].size - 1) * event[:feet_per_grid]}ft."
108
108
  },
109
109
  dodge: lambda { |event|
110
- puts t('event.dodge', name: show_name(event))
110
+ output t('event.dodge', name: show_name(event))
111
111
  },
112
112
  help: lambda { |event|
113
- puts "#{show_name(event)} is helping to attack #{event[:target].name&.colorize(:red)}"
113
+ output "#{show_name(event)} is helping to attack #{event[:target].name&.colorize(:red)}"
114
114
  },
115
115
  second_wind: lambda { |event|
116
- puts SecondWindAction.describe(event)
116
+ output SecondWindAction.describe(event)
117
117
  },
118
118
  heal: lambda { |event|
119
- puts "#{show_name(event)} heals for #{event[:value]}hp"
119
+ output "#{show_name(event)} heals for #{event[:value]}hp"
120
120
  },
121
121
  hit_die: lambda { |event|
122
- puts t('event.hit_die', source: show_name(event), roll: event[:roll].to_s,
122
+ output t('event.hit_die', source: show_name(event), roll: event[:roll].to_s,
123
123
  value: event[:roll].result)
124
124
  },
125
125
  object_interaction: lambda { |event|
126
126
  if event[:roll]
127
- puts "#{event[:roll]} = #{event[:roll].result} -> #{event[:reason]}"
127
+ output "#{event[:roll]} = #{event[:roll].result} -> #{event[:reason]}"
128
128
  else
129
- puts (event[:reason]).to_s
129
+ output (event[:reason]).to_s
130
130
  end
131
131
  },
132
132
  perception: lambda { |event|
133
- puts "#{show_name(event)} rolls #{event[:perception_roll]} on perception"
133
+ output "#{show_name(event)} rolls #{event[:perception_roll]} on perception"
134
134
  },
135
135
 
136
136
  death_save: lambda { |event|
137
- puts t('event.death_save', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
137
+ output t('event.death_save', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
138
138
  saves: event[:saves], fails: event[:fails]).colorize(:blue)
139
139
  },
140
140
 
141
141
  death_fail: lambda { |event|
142
142
  if event[:roll]
143
- puts t('event.death_fail', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
143
+ output t('event.death_fail', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
144
144
  saves: event[:saves], fails: event[:fails]).colorize(:red)
145
145
  else
146
- puts t('event.death_fail_hit', name: show_name(event), saves: event[:saves],
146
+ output t('event.death_fail_hit', name: show_name(event), saves: event[:saves],
147
147
  fails: event[:fails]).colorize(:red)
148
148
  end
149
149
  },
150
150
  great_weapon_fighting_roll: lambda { |event|
151
- puts t('event.great_weapon_fighting_roll', name: show_name(event),
151
+ output t('event.great_weapon_fighting_roll', name: show_name(event),
152
152
  roll: event[:roll], prev_roll: event[:prev_roll])
153
153
  },
154
154
  feature_protection: lambda { |event|
155
- puts t('event.feature_protection', source: event[:source]&.name,
155
+ output t('event.feature_protection', source: event[:source]&.name,
156
156
  target: event[:target]&.name, attacker: event[:attacker]&.name)
157
157
  },
158
158
  prone: lambda { |event|
159
- puts t('event.status.prone', name: show_name(event))
159
+ output t('event.status.prone', name: show_name(event))
160
160
  },
161
161
 
162
162
  stand: lambda { |event|
163
- puts t('event.status.stand', name: show_name(event))
163
+ output t('event.status.stand', name: show_name(event))
164
164
  },
165
165
 
166
166
  %i[acrobatics athletics] => lambda { |event|
167
167
  if event[:success]
168
- puts t("event.#{event[:event]}.success", name: show_name(event), roll: event[:roll],
168
+ output t("event.#{event[:event]}.success", name: show_name(event), roll: event[:roll],
169
169
  value: event[:roll].result)
170
170
  else
171
- puts t("event.#{event[:event]}.failure", name: show_name(event), roll: event[:roll],
171
+ output t("event.#{event[:event]}.failure", name: show_name(event), roll: event[:roll],
172
172
  value: event[:roll].result)
173
173
  end
174
174
  },
175
175
 
176
176
  start_of_combat: lambda { |event|
177
- puts t('event.combat_start')
177
+ output t('event.combat_start')
178
178
  event[:combat_order].each_with_index do |entity_and_initiative, index|
179
179
  entity, initiative = entity_and_initiative
180
- puts "#{index + 1}. #{decorate_name(entity)} #{initiative}"
180
+ output "#{index + 1}. #{decorate_name(entity)} #{initiative}"
181
181
  end
182
182
  },
183
-
183
+ spell_buff: lambda { |event|
184
+ output t('event.spell_buff', source: show_name(event), spell: event[:spell].label, target: event[:target]&.name)
185
+ },
184
186
  end_of_combat: lambda { |_event|
185
- puts t('event.combat_end')
187
+ output t('event.combat_end')
186
188
  },
187
189
  grapple_success: lambda { |event|
188
190
  if event[:target_roll]
189
- puts t('event.grapple_success',
191
+ output t('event.grapple_success',
190
192
  source: show_name(event), target: event[:target]&.name,
191
193
  source_roll: event[:source_roll],
192
194
  source_roll_value: event[:source_roll].result,
193
195
  target_roll: event[:target_roll],
194
196
  target_roll_value: event[:target_roll].result)
195
197
  else
196
- puts t('event.grapple_success_no_roll',
198
+ output t('event.grapple_success_no_roll',
197
199
  source: show_name(event), target: event[:target]&.name)
198
200
  end
199
201
  },
200
202
  first_aid: lambda { |event|
201
- puts t('event.first_aid', name: show_name(event),
203
+ output t('event.first_aid', name: show_name(event),
202
204
  target: event[:target]&.name, roll: event[:roll], value: event[:roll].result)
203
205
  },
204
206
  first_aid_failure: lambda { |event|
205
- puts t('event.first_aid_failure', name: show_name(event),
207
+ output t('event.first_aid_failure', name: show_name(event),
206
208
  target: event[:target]&.name, roll: event[:roll], value: event[:roll].result)
207
209
  },
208
210
  grapple_failure: lambda { |event|
209
- puts t('event.grapple_failure',
211
+ output t('event.grapple_failure',
210
212
  source: show_name(event), target: event[:target]&.name,
211
213
  source_roll: event[:source_roll],
212
214
  source_roll_value: event[:source_roll].result,
@@ -214,11 +216,11 @@ module Natural20
214
216
  target_roll_value: event[:target_roll_value].result)
215
217
  },
216
218
  drop_grapple: lambda { |event|
217
- puts t('event.drop_grapple',
219
+ output t('event.drop_grapple',
218
220
  source: show_name(event), target: event[:target]&.name)
219
221
  },
220
222
  flavor: lambda { |event|
221
- puts t("event.flavor.#{event[:text]}", source: event[:source]&.name,
223
+ output t("event.flavor.#{event[:text]}", source: event[:source]&.name,
222
224
  target: event[:target]&.name)
223
225
  },
224
226
  shove_success: lambda do |event|
@@ -230,9 +232,9 @@ module Natural20
230
232
  target_roll_value: event[:target_roll].result
231
233
  }
232
234
  if event[:knock_prone]
233
- puts t('event.knock_prone_success', opts)
235
+ output t('event.knock_prone_success', opts)
234
236
  else
235
- puts t('event.shove_success', opts)
237
+ output t('event.shove_success', opts)
236
238
  end
237
239
  end,
238
240
  shove_failure: lambda do |event|
@@ -244,13 +246,13 @@ module Natural20
244
246
  target_roll_value: event[:target_roll].result
245
247
  }
246
248
  if event[:knock_prone]
247
- puts t('event.knock_prone_failure', opts)
249
+ output t('event.knock_prone_failure', opts)
248
250
  else
249
- puts t('event.shove_failure', opts)
251
+ output t('event.shove_failure', opts)
250
252
  end
251
253
  end,
252
254
  %i[escape_grapple_success escape_grapple_failure] => lambda { |event|
253
- puts t("event.#{event[:event]}",
255
+ output t("event.#{event[:event]}",
254
256
  source: show_name(event), target: event[:target]&.name,
255
257
  source_roll: event[:source_roll],
256
258
  source_roll_value: event[:source_roll].result,
@@ -267,6 +269,10 @@ module Natural20
267
269
  decorate_name(event[:source])
268
270
  end
269
271
 
272
+ def self.output(string)
273
+ puts "[#{Session.current_session&.game_time || 0}] #{string}"
274
+ end
275
+
270
276
  # @param event [Hash]
271
277
  def self.decorate_name(entity)
272
278
  if @battle && @current_entity_context && entity
@@ -282,7 +288,7 @@ module Natural20
282
288
  end
283
289
 
284
290
  def self.t(token, options = {})
285
- I18n.t(token, options)
291
+ I18n.t(token, **options)
286
292
  end
287
293
  end
288
294
  end
@@ -21,7 +21,7 @@ module ItemLibrary
21
21
  protected
22
22
 
23
23
  def t(key, options = {})
24
- I18n.t(key, options)
24
+ I18n.t(key, **options)
25
25
  end
26
26
  end
27
27
  end
@@ -137,6 +137,10 @@ module Natural20
137
137
  true
138
138
  end
139
139
 
140
+ def prepared_spells
141
+ @properties.fetch(:prepared_spells, [])
142
+ end
143
+
140
144
  def generate_npc_attack_actions(battle, opportunity_attack: false)
141
145
  actions = []
142
146
 
@@ -4,20 +4,21 @@ module Natural20
4
4
  include Natural20::Entity
5
5
  include Natural20::RogueClass
6
6
  include Natural20::FighterClass
7
+ include Natural20::WizardClass
7
8
  include Natural20::HealthFlavor
8
9
  prepend Natural20::Lootable
9
10
  include Multiattack
10
11
 
11
- attr_accessor :hp, :other_counters, :resistances, :experience_points, :class_properties
12
+ attr_accessor :hp, :other_counters, :resistances, :experience_points, :class_properties, :spell_slots
12
13
 
13
- ACTION_LIST = %i[first_aid look attack move dash hide help dodge disengage use_item interact ground_interact inventory disengage_bonus
14
+ ACTION_LIST = %i[spell first_aid look attack move dash hide help dodge disengage use_item interact ground_interact inventory disengage_bonus
14
15
  dash_bonus hide_bonus grapple escape_grapple drop_grapple shove push prone stand short_rest two_weapon_attack].freeze
15
16
 
16
17
  # @param session [Natural20::Session]
17
18
  def initialize(session, properties)
18
19
  @session = session
19
20
  @properties = properties.deep_symbolize_keys!
20
-
21
+ @spell_slots = {}
21
22
  @ability_scores = @properties[:ability]
22
23
  @equipped = @properties[:equipped]
23
24
  @race_properties = YAML.load_file(File.join(session.root_path, 'races',
@@ -65,7 +66,16 @@ module Natural20
65
66
  end
66
67
 
67
68
  def armor_class
68
- equipped_ac
69
+ current_ac = if has_effect?(:ac_override)
70
+ eval_effect(:ac_override, armor_class: equipped_ac)
71
+ else
72
+ equipped_ac
73
+ end
74
+ if has_effect?(:ac_bonus)
75
+ current_ac + eval_effect(:ac_bonus)
76
+ else
77
+ current_ac
78
+ end
69
79
  end
70
80
 
71
81
  def level
@@ -152,7 +162,7 @@ module Natural20
152
162
 
153
163
  all_weapon_proficiencies = weapon_proficiencies
154
164
 
155
- return true if all_weapon_proficiencies.include?(weapon[:name])
165
+ return true if all_weapon_proficiencies.include?(weapon[:name].to_s.underscore)
156
166
 
157
167
  all_weapon_proficiencies&.detect do |prof|
158
168
  weapon[:proficiency_type]&.include?(prof)
@@ -218,7 +228,7 @@ module Natural20
218
228
  %w[ranged_attack melee_attack]
219
229
  end
220
230
 
221
- weapon_attacks = @properties[:equipped].map do |item|
231
+ weapon_attacks = @properties[:equipped]&.map do |item|
222
232
  weapon_detail = session.load_weapon(item)
223
233
  next if weapon_detail.nil?
224
234
  next unless valid_weapon_types.include?(weapon_detail[:type])
@@ -238,7 +248,7 @@ module Natural20
238
248
  end
239
249
 
240
250
  attacks
241
- end.flatten.compact
251
+ end&.flatten&.compact || []
242
252
 
243
253
  unarmed_attack = AttackAction.new(session, self, :attack)
244
254
  unarmed_attack.using = 'unarmed_attack'
@@ -317,6 +327,8 @@ module Natural20
317
327
  action = ShoveAction.new(session, self, type)
318
328
  action.knock_prone = true
319
329
  action
330
+ when :spell
331
+ SpellAction.new(session, self, type)
320
332
  when :two_weapon_attack
321
333
  two_weapon_attack_actions(battle)
322
334
  when :push
@@ -328,7 +340,7 @@ module Natural20
328
340
  end
329
341
 
330
342
  def two_weapon_attack_actions(battle)
331
- @properties[:equipped].each do |item|
343
+ @properties[:equipped]&.each do |item|
332
344
  weapon_detail = session.load_weapon(item)
333
345
  next if weapon_detail.nil?
334
346
  next unless weapon_detail[:type] == 'melee_attack'
@@ -373,16 +385,67 @@ module Natural20
373
385
  false
374
386
  end
375
387
 
388
+ # Returns the number of spell slots
389
+ # @param level [Integer]
390
+ # @return [Integer]
391
+ def spell_slots(level, character_class = nil)
392
+ character_class = @spell_slots.keys.first if character_class.nil?
393
+ @spell_slots[character_class].fetch(level, 0)
394
+ end
395
+
396
+ # Returns the number of spell slots
397
+ # @param level [Integer]
398
+ # @return [Integer]
399
+ def max_spell_slots(level, character_class = nil)
400
+ character_class = @spell_slots.keys.first if character_class.nil?
401
+
402
+ return send(:"max_slots_for_#{character_class}", level) if respond_to?(:"max_slots_for_#{character_class}")
403
+
404
+ 0
405
+ end
406
+
407
+ # Consumes a characters spell slot
408
+ def consume_spell_slot!(level, character_class = nil, qty = 1)
409
+ character_class = @spell_slots.keys.first if character_class.nil?
410
+ if @spell_slots[character_class][level]
411
+ @spell_slots[character_class][level] = [@spell_slots[character_class][level] - qty, 0].max
412
+ end
413
+ end
414
+
376
415
  def pc?
377
416
  true
378
417
  end
379
418
 
419
+ def prepared_spells
420
+ @properties.fetch(:cantrips, []) + @properties.fetch(:prepared_spells, [])
421
+ end
422
+
423
+ # Returns the available spells for the current user
424
+ # @param battle [Natural20::Battle]
425
+ # @return [Hash]
426
+ def available_spells(battle)
427
+ prepared_spells.map do |spell|
428
+ details = session.load_spell(spell)
429
+ next unless details
430
+
431
+ _qty, resource = details[:casting_time].split(':')
432
+
433
+ disable_reason = []
434
+ disable_reason << :no_action if resource == 'action' && battle && battle.ongoing? && total_actions(battle).zero?
435
+ if resource == 'bonus_action' && battle.ongoing? && total_bonus_actions(battle).zero?
436
+ disable_reason << :no_bonus_action
437
+ end
438
+ disable_reason << :no_spell_slot if details[:level].positive? && spell_slots(details[:level]).zero?
439
+
440
+ [spell, details.merge(disabled: disable_reason)]
441
+ end.compact.to_h
442
+ end
443
+
380
444
  # @param hit_die_num [Integer] number of hit die to use
381
445
  def short_rest!(battle, prompt: false)
382
446
  super
383
-
384
- @class_properties.keys do |klass|
385
- send(:"short_rest_for_#{klass}") if respond_to?(:"short_rest_for_#{klass}")
447
+ @class_properties.keys.each do |klass|
448
+ send(:"short_rest_for_#{klass}", battle) if respond_to?(:"short_rest_for_#{klass}")
386
449
  end
387
450
  end
388
451
 
@@ -400,7 +463,7 @@ module Natural20
400
463
  def equipped_ac
401
464
  @equipments ||= YAML.load_file(File.join(session.root_path, 'items', 'equipment.yml')).deep_symbolize_keys!
402
465
 
403
- equipped_meta = @equipped.map { |e| @equipments[e.to_sym] }.compact
466
+ equipped_meta = @equipped&.map { |e| @equipments[e.to_sym] }&.compact || []
404
467
  armor = equipped_meta.detect do |equipment|
405
468
  equipment[:type] == 'armor'
406
469
  end