natural_20 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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