natural_20 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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