natural_20 0.1.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +99 -0
  9. data/Rakefile +6 -0
  10. data/bin/compute_lights +19 -0
  11. data/bin/console +19 -0
  12. data/bin/nat20 +135 -0
  13. data/bin/nat20.cmd +3 -0
  14. data/bin/nat20author +104 -0
  15. data/bin/setup +8 -0
  16. data/char_classes/fighter.yml +45 -0
  17. data/char_classes/rogue.yml +54 -0
  18. data/characters/halfling_rogue.yml +46 -0
  19. data/characters/high_elf_fighter.yml +49 -0
  20. data/fixtures/battle_sim.yml +58 -0
  21. data/fixtures/battle_sim_2.yml +30 -0
  22. data/fixtures/battle_sim_3.yml +26 -0
  23. data/fixtures/battle_sim_4.yml +26 -0
  24. data/fixtures/battle_sim_objects.yml +101 -0
  25. data/fixtures/corridors.yml +24 -0
  26. data/fixtures/elf_rogue.yml +39 -0
  27. data/fixtures/halfling_rogue.yml +41 -0
  28. data/fixtures/high_elf_fighter.yml +49 -0
  29. data/fixtures/human_fighter.yml +48 -0
  30. data/fixtures/path_finding_test.yml +11 -0
  31. data/fixtures/path_finding_test_2.yml +15 -0
  32. data/fixtures/path_finding_test_3.yml +26 -0
  33. data/fixtures/thin_walls.yml +53 -0
  34. data/fixtures/traps.yml +25 -0
  35. data/game.yml +20 -0
  36. data/items/equipment.yml +101 -0
  37. data/items/objects.yml +73 -0
  38. data/items/weapons.yml +297 -0
  39. data/lib/natural_20.rb +68 -0
  40. data/lib/natural_20/actions/action.rb +40 -0
  41. data/lib/natural_20/actions/attack_action.rb +372 -0
  42. data/lib/natural_20/actions/concerns/action_damage.rb +14 -0
  43. data/lib/natural_20/actions/dash_action.rb +46 -0
  44. data/lib/natural_20/actions/disengage_action.rb +53 -0
  45. data/lib/natural_20/actions/dodge_action.rb +45 -0
  46. data/lib/natural_20/actions/escape_grapple_action.rb +97 -0
  47. data/lib/natural_20/actions/first_aid_action.rb +109 -0
  48. data/lib/natural_20/actions/grapple_action.rb +185 -0
  49. data/lib/natural_20/actions/ground_interact_action.rb +74 -0
  50. data/lib/natural_20/actions/help_action.rb +56 -0
  51. data/lib/natural_20/actions/hide_action.rb +53 -0
  52. data/lib/natural_20/actions/interact_action.rb +91 -0
  53. data/lib/natural_20/actions/inventory_action.rb +23 -0
  54. data/lib/natural_20/actions/look_action.rb +63 -0
  55. data/lib/natural_20/actions/move_action.rb +254 -0
  56. data/lib/natural_20/actions/multiattack_action.rb +41 -0
  57. data/lib/natural_20/actions/prone_action.rb +38 -0
  58. data/lib/natural_20/actions/short_rest_action.rb +53 -0
  59. data/lib/natural_20/actions/shove_action.rb +142 -0
  60. data/lib/natural_20/actions/stand_action.rb +47 -0
  61. data/lib/natural_20/actions/use_item_action.rb +57 -0
  62. data/lib/natural_20/ai_controller/path_compute.rb +140 -0
  63. data/lib/natural_20/ai_controller/standard.rb +288 -0
  64. data/lib/natural_20/battle.rb +544 -0
  65. data/lib/natural_20/battle_map.rb +843 -0
  66. data/lib/natural_20/cli/builder/fighter_builder.rb +104 -0
  67. data/lib/natural_20/cli/builder/rogue_builder.rb +62 -0
  68. data/lib/natural_20/cli/character_builder.rb +210 -0
  69. data/lib/natural_20/cli/commandline_ui.rb +612 -0
  70. data/lib/natural_20/cli/inventory_ui.rb +136 -0
  71. data/lib/natural_20/cli/map_renderer.rb +165 -0
  72. data/lib/natural_20/concerns/container.rb +32 -0
  73. data/lib/natural_20/concerns/entity.rb +1213 -0
  74. data/lib/natural_20/concerns/evaluator/entity_state_evaluator.rb +59 -0
  75. data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +51 -0
  76. data/lib/natural_20/concerns/fighter_class.rb +35 -0
  77. data/lib/natural_20/concerns/health_flavor.rb +27 -0
  78. data/lib/natural_20/concerns/lootable.rb +94 -0
  79. data/lib/natural_20/concerns/movement_helper.rb +195 -0
  80. data/lib/natural_20/concerns/multiattack.rb +54 -0
  81. data/lib/natural_20/concerns/navigation.rb +87 -0
  82. data/lib/natural_20/concerns/notable.rb +37 -0
  83. data/lib/natural_20/concerns/rogue_class.rb +26 -0
  84. data/lib/natural_20/controller.rb +11 -0
  85. data/lib/natural_20/die_roll.rb +331 -0
  86. data/lib/natural_20/event_manager.rb +288 -0
  87. data/lib/natural_20/item_library/base_item.rb +27 -0
  88. data/lib/natural_20/item_library/chest.rb +230 -0
  89. data/lib/natural_20/item_library/door_object.rb +189 -0
  90. data/lib/natural_20/item_library/ground.rb +124 -0
  91. data/lib/natural_20/item_library/healing_potion.rb +51 -0
  92. data/lib/natural_20/item_library/object.rb +153 -0
  93. data/lib/natural_20/item_library/pit_trap.rb +69 -0
  94. data/lib/natural_20/item_library/stone_wall.rb +18 -0
  95. data/lib/natural_20/npc.rb +173 -0
  96. data/lib/natural_20/player_character.rb +414 -0
  97. data/lib/natural_20/session.rb +168 -0
  98. data/lib/natural_20/utils/cover.rb +35 -0
  99. data/lib/natural_20/utils/ray_tracer.rb +90 -0
  100. data/lib/natural_20/utils/static_light_builder.rb +72 -0
  101. data/lib/natural_20/utils/weapons.rb +78 -0
  102. data/lib/natural_20/version.rb +4 -0
  103. data/locales/en.yml +304 -0
  104. data/maps/game_map.yml +168 -0
  105. data/natural_20.gemspec +46 -0
  106. data/npcs/goblin.yml +64 -0
  107. data/npcs/human_guard.yml +48 -0
  108. data/npcs/ogre.yml +61 -0
  109. data/npcs/owlbear.yml +55 -0
  110. data/npcs/wolf.yml +46 -0
  111. data/races/elf.yml +44 -0
  112. data/races/halfling.yml +22 -0
  113. data/races/human.yml +13 -0
  114. metadata +373 -0
@@ -0,0 +1,54 @@
1
+ # typed: false
2
+ module Multiattack
3
+ def setup_attributes
4
+ super
5
+ end
6
+
7
+ # Get available multiattack actions
8
+ # @param session [Natural20::Session]
9
+ # @param battle [Natural20::Battle]
10
+ # @return [Array]
11
+ def multi_attack_actions(session, battle)
12
+ end
13
+
14
+ # @param battle [Natural20::Battle]
15
+ def reset_turn!(battle)
16
+ entity_state = super battle
17
+
18
+ return entity_state unless class_feature?("multiattack")
19
+
20
+ multiattack_groups = {}
21
+ @properties[:actions].select { |a| a[:multiattack_group] }.each do |a|
22
+ multiattack_groups[a[:multiattack_group]] ||= []
23
+ multiattack_groups[a[:multiattack_group]] << a[:name]
24
+ end
25
+
26
+ entity_state[:multiattack] = multiattack_groups
27
+
28
+ entity_state
29
+ end
30
+
31
+ # @param battle [Natural20::Battle]
32
+ def clear_multiattack!(battle)
33
+ entity_state = battle.entity_state_for(self)
34
+ entity_state[:multiattack] = {}
35
+ end
36
+
37
+ # @param battle [Natural20::Battle]
38
+ # @param npc_action [Hash]
39
+ def multiattack?(battle, npc_action)
40
+ return false unless npc_action
41
+ return false unless class_feature?("multiattack")
42
+
43
+ entity_state = battle.entity_state_for(self)
44
+
45
+ return false unless entity_state[:multiattack]
46
+ return false unless npc_action[:multiattack_group]
47
+
48
+ entity_state[:multiattack].each do |_group, attacks|
49
+ return true if attacks.include?(npc_action[:name])
50
+ end
51
+
52
+ false
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ module Natural20::Navigation
2
+ include Natural20::Weapons
3
+
4
+ # @param map [natural20::BattleMap]
5
+ # @param battle [Natural20::Battle]
6
+ # @param entity [Natural20::Entity]
7
+ def candidate_squares(map, battle, entity)
8
+ compute = AiController::PathCompute.new(battle, map, entity)
9
+ cur_pos_x, cur_pos_y = map.entity_or_object_pos(entity)
10
+
11
+ compute.build_structures(cur_pos_x, cur_pos_y)
12
+ compute.path
13
+
14
+ candidate_squares = [[[cur_pos_x, cur_pos_y], 0]]
15
+ map.size[0].times.each do |pos_x|
16
+ map.size[1].times.each do |pos_y|
17
+ next unless map.line_of_sight_for?(entity, pos_x, pos_y)
18
+ next unless map.placeable?(entity, pos_x, pos_y, battle)
19
+
20
+ path, cost = compute.incremental_path(cur_pos_x, cur_pos_y, pos_x, pos_y)
21
+ next if path.nil?
22
+
23
+ candidate_squares << [[pos_x, pos_y], cost.floor]
24
+ end
25
+ end
26
+ candidate_squares.uniq.to_h
27
+ end
28
+
29
+ # @param map [Natural20::BattleMap]
30
+ # @param battle [Natural20::Battle]
31
+ # @param entity [Natural20::Entity]
32
+ # @param opponents [Array<Natural20::Entity>]
33
+ def evaluate_square(map, battle, entity, opponents)
34
+ melee_attack_squares = {}
35
+ opponents.each do |opp|
36
+ opp.melee_squares(map).each do |pos|
37
+ melee_attack_squares[pos] ||= 0
38
+ melee_attack_squares[pos] += 1
39
+ end
40
+ end
41
+
42
+ attack_options = if entity.npc?
43
+ entity.npc_actions.map do |npc_action|
44
+ next if npc_action[:ammo] && entity.item_count(npc_action[:ammo]) <= 0
45
+ next if npc_action[:if] && !entity.eval_if(npc_action[:if])
46
+ next unless npc_action[:type] == 'melee_attack'
47
+
48
+ npc_action
49
+ end.first
50
+ end
51
+
52
+ destinations = candidate_squares(map, battle, entity)
53
+ destinations.map do |d, _cost|
54
+ # evaluate defense
55
+ melee_offence = 0.0
56
+ ranged_offence = 0.0
57
+ defense = 0.0
58
+ mobility = 0.0
59
+ support = 0.0
60
+
61
+ if melee_attack_squares.key?(d)
62
+ melee_offence += 0.1
63
+ defense -= 0.05 * melee_attack_squares[d]
64
+ if attack_options
65
+ opponents.each do |opp|
66
+ adv, _adv_info = target_advantage_condition(battle, entity, opp, attack_options, source_pos: d)
67
+ melee_offence += adv
68
+ end
69
+ end
70
+ else
71
+ ranged_offence += 0.1
72
+ opponents.each do |opp|
73
+ defense += map.cover_calculation(map, opp, entity, entity_2_pos: d,
74
+ naturally_stealthy: entity.class_feature?('naturally_stealthy')).to_f
75
+ end
76
+ end
77
+
78
+ if map.requires_squeeze?(entity, *d, map, battle)
79
+ mobility -= 1.0
80
+ melee_offence -= 0.5
81
+ ranged_offence -= 0.5
82
+ end
83
+
84
+ [d, [melee_offence, ranged_offence, defense, mobility, support]]
85
+ end.to_h
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ module Natural20
2
+ # Concerns used for objects that can have notes
3
+ module Notable
4
+ # List notes on object
5
+ # @param entity [Natural20::Entity]
6
+ # @param perception [Integer]
7
+ # @return [Array]
8
+ def list_notes(entity, perception, highlight: false)
9
+ @properties[:notes]&.map do |note|
10
+ next if highlight && !note[:highlight]
11
+ next if note[:if].presence && !eval_if(note[:if])
12
+
13
+ perception_dc = note[:perception_dc] || 0
14
+ if perception >= perception_dc
15
+ note_language = note[:language].presence
16
+
17
+ result = if note_language
18
+ note_content = if entity.languages.include?(note_language)
19
+ note[:note]
20
+ else
21
+ '???'
22
+ end
23
+ t('perception.note_with_language', note_language: note_language, note: note_content)
24
+ else
25
+ note[:note]
26
+ end
27
+
28
+ if perception_dc.positive?
29
+ t('perception.passed', note: result)
30
+ else
31
+ result
32
+ end
33
+ end
34
+ end&.compact || []
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # typed: false
2
+ module Natural20::RogueClass
3
+ attr_accessor :rogue_level
4
+
5
+ def initialize_rogue
6
+ end
7
+
8
+ def sneak_attack_level
9
+ [
10
+ "1d6", "1d6",
11
+ "2d6", "2d6",
12
+ "3d6", "3d6",
13
+ "4d6", "4d6",
14
+ "5d6", "5d6",
15
+ "6d6", "6d6",
16
+ "7d6", "7d6",
17
+ "8d6", "8d6",
18
+ "9d6", "9d6",
19
+ "10d6", "10d6",
20
+ ][level]
21
+ end
22
+
23
+ def special_actions_for_rogue(session, battle)
24
+ []
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Natural20
2
+ class Controller
3
+ def roll_for(entity, die_type, number_of_times, description, advantage: false, disadvantage: false); end
4
+
5
+ # Return moves by a player using the commandline UI
6
+ # @param entity [Natural20::Entity] The entity to compute moves for
7
+ # @param battle [Natural20::Battle] An instance of the current battle
8
+ # @return [Array(Natural20::Action)]
9
+ def move_for(entity, battle); end
10
+ end
11
+ end
@@ -0,0 +1,331 @@
1
+ # typed: true
2
+ module Natural20
3
+ class DieRollDetail
4
+ # @return [Integer]
5
+ attr_accessor :die_count
6
+
7
+ # @return [String]
8
+ attr_accessor :die_type
9
+
10
+ # @return [Integer]
11
+ attr_accessor :modifier
12
+
13
+ # @return [Symbol]
14
+ attr_accessor :modifier_op
15
+ end
16
+
17
+ class Roller
18
+ attr_reader :roll_str, :crit, :advantage, :disadvantage, :description, :entity, :battle
19
+
20
+ def initialize(roll_str, crit: false, disadvantage: false, advantage: false, description: nil, entity: nil, battle: nil)
21
+ @roll_str = roll_str
22
+ @crit = crit
23
+ @advantage = advantage
24
+ @disadvantage = disadvantage
25
+ @description = description
26
+ @entity = entity
27
+ @battle = battle
28
+ end
29
+
30
+ # @param lucky [Boolean] This is a lucky feat reroll
31
+ def roll(lucky: false, description_override: nil)
32
+ die_sides = 20
33
+
34
+ detail = DieRoll.parse(roll_str)
35
+ number_of_die = detail.die_count
36
+ die_type_str = detail.die_type
37
+ modifier_str = detail.modifier
38
+ modifier_op = detail.modifier_op
39
+
40
+ if die_type_str.blank?
41
+ return Natural20::DieRoll.new([number_of_die], "#{modifier_op}#{modifier_str}".to_i, 0,
42
+ roller: self)
43
+ end
44
+
45
+ die_sides = die_type_str.to_i
46
+
47
+ number_of_die *= 2 if crit
48
+
49
+ description = t('dice_roll.description', description: description_override.presence || @description,
50
+ roll_str: roll_str)
51
+ description = lucky ? "(lucky) #{description}" : description
52
+
53
+ description = '(with advantage)'.colorize(:blue) + description if advantage
54
+ description = '(with disadvantage)'.colorize(:red) + description if disadvantage
55
+ rolls = if advantage || disadvantage
56
+ if battle
57
+ battle.roll_for(entity, die_sides, number_of_die, description, advantage: advantage,
58
+ disadvantage: disadvantage)
59
+ else
60
+ number_of_die.times.map { [(1..die_sides).to_a.sample, (1..die_sides).to_a.sample] }
61
+ end
62
+ elsif battle
63
+ battle.roll_for(entity, die_sides, number_of_die, description)
64
+ else
65
+ number_of_die.times.map { (1..die_sides).to_a.sample }
66
+ end
67
+ Natural20::DieRoll.new(rolls, modifier_str.blank? ? 0 : "#{modifier_op}#{modifier_str}".to_i, die_sides,
68
+ advantage: advantage, disadvantage: disadvantage, roller: self)
69
+ end
70
+
71
+ def t(key, options = {})
72
+ I18n.t(key, options)
73
+ end
74
+ end
75
+
76
+ class DieRoll
77
+ class DieRolls
78
+ attr_accessor :rolls
79
+
80
+ def initialize(rolls = [])
81
+ @rolls = rolls
82
+ end
83
+
84
+ def add_to_front(die_roll)
85
+ if die_roll.is_a?(Natural20::DieRoll)
86
+ @rolls.unshift(die_roll)
87
+ elsif die_roll.is_a?(DieRolls)
88
+ @rolls = die_roll.rolls + @rolls
89
+ end
90
+ end
91
+
92
+ def +(other)
93
+ if other.is_a?(Natural20::DieRoll)
94
+ @rolls << other
95
+ elsif other.is_a?(DieRolls)
96
+ @rolls += other.rolls
97
+ end
98
+ end
99
+
100
+ def result
101
+ @rolls.inject(0) do |sum, roll|
102
+ sum + roll.result
103
+ end
104
+ end
105
+
106
+ def ==(other)
107
+ return false if other.rolls.size != @rolls.size
108
+
109
+ @rolls.each_with_index do |roll, index|
110
+ return false if other.rolls[index] != roll
111
+ end
112
+
113
+ true
114
+ end
115
+
116
+ def to_s
117
+ @rolls.map(&:to_s).join(' + ')
118
+ end
119
+ end
120
+
121
+ attr_reader :rolls, :modifier, :die_sides, :roller
122
+
123
+ # This represents a dice roll
124
+ # @param rolls [Array] Integer dice roll representations
125
+ # @param modifier [Integer] a constant value to add to the roll
126
+ def initialize(rolls, modifier, die_sides = 20, advantage: false, disadvantage: false, description: nil, roller: nil)
127
+ @rolls = rolls
128
+ @modifier = modifier
129
+ @die_sides = die_sides
130
+ @advantage = advantage
131
+ @disadvantage = disadvantage
132
+ @description = description
133
+ @roller = roller
134
+ end
135
+
136
+ # This is a natural 20 or critical roll
137
+ # @return [Boolean]
138
+ def nat_20?
139
+ if @advantage
140
+ @rolls.map(&:max).detect { |r| r == 20 }
141
+ elsif @disadvantage
142
+ @rolls.map(&:min).detect { |r| r == 20 }
143
+ else
144
+ @rolls.include?(20)
145
+ end
146
+ end
147
+
148
+ def nat_1?
149
+ if @advantage
150
+ @rolls.map(&:max).detect { |r| r == 1 }
151
+ elsif @disadvantage
152
+ @rolls.map(&:min).detect { |r| r == 1 }
153
+ else
154
+ @rolls.include?(1)
155
+ end
156
+ end
157
+
158
+ def reroll(lucky: false)
159
+ @roller.roll(lucky: lucky)
160
+ end
161
+
162
+ # computes the integer result of the dice roll
163
+ # @return [Integer]
164
+ def result
165
+ sum = if @advantage
166
+ @rolls.map(&:max).sum
167
+ elsif @disadvantage
168
+ @rolls.map(&:min).sum
169
+ else
170
+ @rolls.sum
171
+ end
172
+
173
+ sum + @modifier
174
+ end
175
+
176
+ # adds color flair to the roll depending on value
177
+ # @param roll [String,Integer]
178
+ # @return [String]
179
+ def color_roll(roll)
180
+ case roll
181
+ when 1
182
+ roll.to_s.colorize(:red)
183
+ when @die_sides
184
+ roll.to_s.colorize(:green)
185
+ else
186
+ roll.to_s
187
+ end
188
+ end
189
+
190
+ def to_s
191
+ rolls = @rolls.map do |r|
192
+ if @advantage
193
+ r.map do |i|
194
+ i == r.max ? color_roll(i).bold : i.to_s.colorize(:gray)
195
+ end.join(' | ')
196
+ elsif @disadvantage
197
+ r.map do |i|
198
+ i == r.min ? color_roll(i).bold : i.to_s.colorize(:gray)
199
+ end.join(' | ')
200
+ else
201
+ color_roll(r)
202
+ end
203
+ end
204
+
205
+ if @modifier != 0
206
+ "(#{rolls.join(' + ')}) + #{@modifier}"
207
+ else
208
+ "(#{rolls.join(' + ')})"
209
+ end
210
+ end
211
+
212
+ def self.numeric?(c)
213
+ return true if c =~ /\A\d+\Z/
214
+
215
+ begin
216
+ true if Float(c)
217
+ rescue StandardError
218
+ false
219
+ end
220
+ end
221
+
222
+ def ==(other)
223
+ return true if other.rolls == @rolls && other.modifier == @modifier && other.die_sides == @die_sides
224
+
225
+ false
226
+ end
227
+
228
+ def <=>(other)
229
+ result <=> other.result
230
+ end
231
+
232
+ def +(other)
233
+ if other.is_a?(DieRolls)
234
+ other.add_to_front(self)
235
+ other
236
+ else
237
+ DieRolls.new([self, other])
238
+ end
239
+ end
240
+
241
+ # @param die_roll_str [String]
242
+ # @return [Natural20::DieRollDetail]
243
+ def self.parse(roll_str)
244
+ die_count_str = ''
245
+ die_type_str = ''
246
+ modifier_str = ''
247
+ modifier_op = ''
248
+ state = :initial
249
+
250
+ roll_str.strip.each_char do |c|
251
+ case state
252
+ when :initial
253
+ if numeric?(c)
254
+ die_count_str << c
255
+ elsif c == 'd'
256
+ state = :die_type
257
+ elsif c == '+'
258
+ state = :modifier
259
+ end
260
+ when :die_type
261
+ next if c == ' '
262
+
263
+ if numeric?(c)
264
+ die_type_str << c
265
+ elsif c == '+'
266
+ state = :modifier
267
+ elsif c == '-'
268
+ modifier_op = '-'
269
+ state = :modifier
270
+ end
271
+ when :modifier
272
+ next if c == ' '
273
+
274
+ modifier_str << c if numeric?(c)
275
+ end
276
+ end
277
+
278
+ if state == :initial
279
+ modifier_str = die_count_str
280
+ die_count_str = '0'
281
+ end
282
+
283
+ number_of_die = die_count_str.blank? ? 1 : die_count_str.to_i
284
+
285
+ detail = Natural20::DieRollDetail.new
286
+ detail.die_count = number_of_die
287
+ detail.die_type = die_type_str
288
+ detail.modifier = modifier_str
289
+ detail.modifier_op = modifier_op
290
+ detail
291
+ end
292
+
293
+ # Rolls the dice, details on dice rolls and its values are preserved
294
+ # @param roll_str [String] A dice roll expression
295
+ # @param entity [Natural20::Entity]
296
+ # @param crit [Boolean] A critial hit damage roll - double dice rolls
297
+ # @param advantage [Boolean] Roll with advantage, roll twice and select the highest
298
+ # @param disadvantage [Boolean] Roll with disadvantage, roll twice and select the lowest
299
+ # @param battle [Natural20::Battle]
300
+ # @return [Natural20::DieRoll]
301
+ def self.roll(roll_str, crit: false, disadvantage: false, advantage: false, description: nil, entity: nil, battle: nil)
302
+ roller = Roller.new(roll_str, crit: crit, disadvantage: disadvantage, advantage: advantage,
303
+ description: description, entity: entity, battle: battle)
304
+ roller.roll
305
+ end
306
+
307
+ # Rolls the dice checking lucky feat, details on dice rolls and its values are preserved
308
+ # @param entity [Natural20::Entity]
309
+ # @param roll_str [String] A dice roll expression
310
+ # @param entity [Natural20::Entity]
311
+ # @param crit [Boolean] A critial hit damage roll - double dice rolls
312
+ # @param advantage [Boolean] Roll with advantage, roll twice and select the highest
313
+ # @param disadvantage [Boolean] Roll with disadvantage, roll twice and select the lowest
314
+ # @param battle [Natural20::Battle]
315
+ # @return [Natural20::DieRoll]
316
+ def self.roll_with_lucky(entity, roll_str, crit: false, disadvantage: false, advantage: false, description: nil, battle: nil)
317
+ roller = Roller.new(roll_str, crit: crit, disadvantage: disadvantage, advantage: advantage,
318
+ description: description, entity: entity, battle: battle)
319
+ result = roller.roll
320
+ if result.nat_1? && entity.class_feature?('lucky')
321
+ roller.roll(lucky: true)
322
+ else
323
+ result
324
+ end
325
+ end
326
+
327
+ def self.t(key, options = {})
328
+ I18n.t(key, options)
329
+ end
330
+ end
331
+ end