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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/compute_lights +19 -0
- data/bin/console +19 -0
- data/bin/nat20 +135 -0
- data/bin/nat20.cmd +3 -0
- data/bin/nat20author +104 -0
- data/bin/setup +8 -0
- data/char_classes/fighter.yml +45 -0
- data/char_classes/rogue.yml +54 -0
- data/characters/halfling_rogue.yml +46 -0
- data/characters/high_elf_fighter.yml +49 -0
- data/fixtures/battle_sim.yml +58 -0
- data/fixtures/battle_sim_2.yml +30 -0
- data/fixtures/battle_sim_3.yml +26 -0
- data/fixtures/battle_sim_4.yml +26 -0
- data/fixtures/battle_sim_objects.yml +101 -0
- data/fixtures/corridors.yml +24 -0
- data/fixtures/elf_rogue.yml +39 -0
- data/fixtures/halfling_rogue.yml +41 -0
- data/fixtures/high_elf_fighter.yml +49 -0
- data/fixtures/human_fighter.yml +48 -0
- data/fixtures/path_finding_test.yml +11 -0
- data/fixtures/path_finding_test_2.yml +15 -0
- data/fixtures/path_finding_test_3.yml +26 -0
- data/fixtures/thin_walls.yml +53 -0
- data/fixtures/traps.yml +25 -0
- data/game.yml +20 -0
- data/items/equipment.yml +101 -0
- data/items/objects.yml +73 -0
- data/items/weapons.yml +297 -0
- data/lib/natural_20.rb +68 -0
- data/lib/natural_20/actions/action.rb +40 -0
- data/lib/natural_20/actions/attack_action.rb +372 -0
- data/lib/natural_20/actions/concerns/action_damage.rb +14 -0
- data/lib/natural_20/actions/dash_action.rb +46 -0
- data/lib/natural_20/actions/disengage_action.rb +53 -0
- data/lib/natural_20/actions/dodge_action.rb +45 -0
- data/lib/natural_20/actions/escape_grapple_action.rb +97 -0
- data/lib/natural_20/actions/first_aid_action.rb +109 -0
- data/lib/natural_20/actions/grapple_action.rb +185 -0
- data/lib/natural_20/actions/ground_interact_action.rb +74 -0
- data/lib/natural_20/actions/help_action.rb +56 -0
- data/lib/natural_20/actions/hide_action.rb +53 -0
- data/lib/natural_20/actions/interact_action.rb +91 -0
- data/lib/natural_20/actions/inventory_action.rb +23 -0
- data/lib/natural_20/actions/look_action.rb +63 -0
- data/lib/natural_20/actions/move_action.rb +254 -0
- data/lib/natural_20/actions/multiattack_action.rb +41 -0
- data/lib/natural_20/actions/prone_action.rb +38 -0
- data/lib/natural_20/actions/short_rest_action.rb +53 -0
- data/lib/natural_20/actions/shove_action.rb +142 -0
- data/lib/natural_20/actions/stand_action.rb +47 -0
- data/lib/natural_20/actions/use_item_action.rb +57 -0
- data/lib/natural_20/ai_controller/path_compute.rb +140 -0
- data/lib/natural_20/ai_controller/standard.rb +288 -0
- data/lib/natural_20/battle.rb +544 -0
- data/lib/natural_20/battle_map.rb +843 -0
- data/lib/natural_20/cli/builder/fighter_builder.rb +104 -0
- data/lib/natural_20/cli/builder/rogue_builder.rb +62 -0
- data/lib/natural_20/cli/character_builder.rb +210 -0
- data/lib/natural_20/cli/commandline_ui.rb +612 -0
- data/lib/natural_20/cli/inventory_ui.rb +136 -0
- data/lib/natural_20/cli/map_renderer.rb +165 -0
- data/lib/natural_20/concerns/container.rb +32 -0
- data/lib/natural_20/concerns/entity.rb +1213 -0
- data/lib/natural_20/concerns/evaluator/entity_state_evaluator.rb +59 -0
- data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +51 -0
- data/lib/natural_20/concerns/fighter_class.rb +35 -0
- data/lib/natural_20/concerns/health_flavor.rb +27 -0
- data/lib/natural_20/concerns/lootable.rb +94 -0
- data/lib/natural_20/concerns/movement_helper.rb +195 -0
- data/lib/natural_20/concerns/multiattack.rb +54 -0
- data/lib/natural_20/concerns/navigation.rb +87 -0
- data/lib/natural_20/concerns/notable.rb +37 -0
- data/lib/natural_20/concerns/rogue_class.rb +26 -0
- data/lib/natural_20/controller.rb +11 -0
- data/lib/natural_20/die_roll.rb +331 -0
- data/lib/natural_20/event_manager.rb +288 -0
- data/lib/natural_20/item_library/base_item.rb +27 -0
- data/lib/natural_20/item_library/chest.rb +230 -0
- data/lib/natural_20/item_library/door_object.rb +189 -0
- data/lib/natural_20/item_library/ground.rb +124 -0
- data/lib/natural_20/item_library/healing_potion.rb +51 -0
- data/lib/natural_20/item_library/object.rb +153 -0
- data/lib/natural_20/item_library/pit_trap.rb +69 -0
- data/lib/natural_20/item_library/stone_wall.rb +18 -0
- data/lib/natural_20/npc.rb +173 -0
- data/lib/natural_20/player_character.rb +414 -0
- data/lib/natural_20/session.rb +168 -0
- data/lib/natural_20/utils/cover.rb +35 -0
- data/lib/natural_20/utils/ray_tracer.rb +90 -0
- data/lib/natural_20/utils/static_light_builder.rb +72 -0
- data/lib/natural_20/utils/weapons.rb +78 -0
- data/lib/natural_20/version.rb +4 -0
- data/locales/en.yml +304 -0
- data/maps/game_map.yml +168 -0
- data/natural_20.gemspec +46 -0
- data/npcs/goblin.yml +64 -0
- data/npcs/human_guard.yml +48 -0
- data/npcs/ogre.yml +61 -0
- data/npcs/owlbear.yml +55 -0
- data/npcs/wolf.yml +46 -0
- data/races/elf.yml +44 -0
- data/races/halfling.yml +22 -0
- data/races/human.yml +13 -0
- 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
|