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