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,45 @@
1
+ # typed: true
2
+ class DodgeAction < Natural20::Action
3
+ attr_accessor :as_bonus_action
4
+
5
+ def self.can?(entity, battle)
6
+ battle && entity.total_actions(battle).positive?
7
+ end
8
+
9
+ def build_map
10
+ OpenStruct.new({
11
+ param: nil,
12
+ next: -> { self }
13
+ })
14
+ end
15
+
16
+ def self.build(session, source)
17
+ action = DodgeAction.new(session, source, :attack)
18
+ action.build_map
19
+ end
20
+
21
+ def resolve(_session, _map, opts = {})
22
+ @result = [{
23
+ source: @source,
24
+ type: :dodge,
25
+ battle: opts[:battle]
26
+ }]
27
+ self
28
+ end
29
+
30
+ def apply!(battle)
31
+ @result.each do |item|
32
+ case (item[:type])
33
+ when :dodge
34
+ Natural20::EventManager.received_event({ source: item[:source], event: :dodge })
35
+ item[:source].dodging!(battle)
36
+ end
37
+
38
+ if as_bonus_action
39
+ battle.entity_state_for(item[:source])[:bonus_action] -= 1
40
+ else
41
+ battle.entity_state_for(item[:source])[:action] -= 1
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,97 @@
1
+ class EscapeGrappleAction < Natural20::Action
2
+ attr_accessor :target
3
+
4
+ def self.can?(entity, battle, _options = {})
5
+ entity.grappled? && (battle.nil? || entity.total_actions(battle).positive?)
6
+ end
7
+
8
+ def to_s
9
+ @action_type.to_s.humanize
10
+ end
11
+
12
+ def build_map
13
+ OpenStruct.new({
14
+ action: self,
15
+ param: [
16
+ {
17
+ type: :select_target,
18
+ targets: @source.grapples,
19
+ num: 1
20
+ }
21
+ ],
22
+ next: lambda { |target|
23
+ self.target = target
24
+ OpenStruct.new({
25
+ param: nil,
26
+ next: -> { self }
27
+ })
28
+ }
29
+ })
30
+ end
31
+
32
+ def self.build(session, source)
33
+ action = EscapeGrappleAction.new(session, source, :escape_grapple)
34
+ action.build_map
35
+ end
36
+
37
+ # Build the attack roll information
38
+ # @param session [Natural20::Session]
39
+ # @param map [Natural20::BattleMap]
40
+ # @option opts battle [Natural20::Battle]
41
+ # @option opts target [Natural20::Entity]
42
+ def resolve(_session, _map, opts = {})
43
+ battle = opts[:battle]
44
+ target = @source.grapples.first
45
+
46
+ strength_roll = target.athletics_check!(battle)
47
+ athletics_stats = (@source.athletics_proficient? ? @source.proficiency_bonus : 0) + @source.str_mod
48
+ acrobatics_stats = (@source.acrobatics_proficient? ? @source.proficiency_bonus : 0) + @source.dex_mod
49
+
50
+ contested_roll = athletics_stats > acrobatics_stats ? @source.athletics_check!(battle) : @source.acrobatics_check!(battle)
51
+ grapple_success = strength_roll.result >= contested_roll.result
52
+
53
+ @result = if grapple_success
54
+ [{
55
+ source: @source,
56
+ target: target,
57
+ type: :grapple_escape,
58
+ success: true,
59
+ battle: battle,
60
+ source_roll: contested_roll,
61
+ target_roll: strength_roll
62
+ }]
63
+ else
64
+ [{
65
+ source: @source,
66
+ target: target,
67
+ type: :grapple_escape,
68
+ success: false,
69
+ battle: battle,
70
+ source_roll: contested_roll,
71
+ target_roll: strength_roll
72
+ }]
73
+ end
74
+ end
75
+
76
+ def apply!(battle)
77
+ @result.each do |item|
78
+ case (item[:type])
79
+ when :grapple_escape
80
+ if item[:success]
81
+ @source.escape_grapple_from!(item[:target])
82
+ Natural20::EventManager.received_event(event: :escape_grapple_success,
83
+ target: item[:target], source: @source,
84
+ source_roll: item[:source_roll],
85
+ target_roll: item[:target_roll])
86
+ else
87
+ Natural20::EventManager.received_event(event: :escape_grapple_failure,
88
+ target: item[:target], source: @source,
89
+ source_roll: item[:source_roll],
90
+ target_roll: item[:target_roll])
91
+ end
92
+
93
+ battle.entity_state_for(item[:source])[:action] -= 1
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,109 @@
1
+ class FirstAidAction < Natural20::Action
2
+ attr_accessor :target
3
+
4
+ def self.can?(entity, battle, _options = {})
5
+ (battle.nil? || entity.total_actions(battle).positive?) && unconscious_targets(entity, battle).size.positive?
6
+ end
7
+
8
+ # @param entity [Natural20::Entity]
9
+ # @param battle [Natural20::Battle]
10
+ def self.unconscious_targets(entity, battle)
11
+ return [] unless battle
12
+
13
+ adjacent_squares = entity.melee_squares(battle.map, adjacent_only: true)
14
+ entities = []
15
+ adjacent_squares.select do |pos|
16
+ entity_pos = battle.map.entity_at(*pos)
17
+ next unless entity_pos
18
+ next if entity_pos == entity
19
+ next unless battle.map.can_see?(entity, entity_pos)
20
+
21
+ entities << entity_pos if entity_pos.unconscious? && !entity_pos.stable? && !entity_pos.dead?
22
+ end
23
+ entities
24
+ end
25
+
26
+ def to_s
27
+ @action_type.to_s.humanize
28
+ end
29
+
30
+ def build_map
31
+ OpenStruct.new({
32
+ action: self,
33
+ param: [
34
+ {
35
+ type: :select_target,
36
+ range: 5,
37
+ target_types: %i[enemies allies],
38
+ filter: 'state:unconscious & state:!stable & state:!dead',
39
+ num: 1
40
+ }
41
+ ],
42
+ next: lambda { |target|
43
+ self.target = target
44
+ OpenStruct.new({
45
+ param: nil,
46
+ next: -> { self }
47
+ })
48
+ }
49
+ })
50
+ end
51
+
52
+ def self.build(session, source)
53
+ action = FirstAidAction.new(session, source, :grapple)
54
+ action.build_map
55
+ end
56
+
57
+ # Build the attack roll information
58
+ # @param session [Natural20::Session]
59
+ # @param map [Natural20::BattleMap]
60
+ # @option opts battle [Natural20::Battle]
61
+ # @option opts target [Natural20::Entity]
62
+ def resolve(_session, _map, opts = {})
63
+ target = opts[:target] || @target
64
+ battle = opts[:battle]
65
+ raise 'target is a required option for :first_aid' if target.nil?
66
+
67
+ medicine_check = @source.medicine_check!(battle)
68
+
69
+ @result = if medicine_check.result >= 10
70
+ [{
71
+ source: @source,
72
+ target: target,
73
+ type: :first_aid,
74
+ success: true,
75
+ battle: battle,
76
+ roll: medicine_check
77
+ }]
78
+ else
79
+ [{
80
+ source: @source,
81
+ target: target,
82
+ type: :first_aid,
83
+ success: false,
84
+ battle: battle,
85
+ roll: medicine_check
86
+ }]
87
+ end
88
+ end
89
+
90
+ def apply!(battle)
91
+ @result.each do |item|
92
+ case (item[:type])
93
+ when :first_aid
94
+ if item[:success]
95
+ item[:target].stable!
96
+ Natural20::EventManager.received_event(event: :first_aid,
97
+ target: item[:target], source: @source,
98
+ roll: item[:roll])
99
+ else
100
+ Natural20::EventManager.received_event(event: :first_aid_failure,
101
+ target: item[:target], source: @source,
102
+ roll: item[:roll])
103
+ end
104
+
105
+ battle.entity_state_for(item[:source])[:action] -= 1
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,185 @@
1
+ class GrappleAction < Natural20::Action
2
+ attr_accessor :target
3
+
4
+ def self.can?(entity, battle, _options = {})
5
+ (battle.nil? || entity.total_actions(battle).positive?) && !entity.grappling?
6
+ end
7
+
8
+ def to_s
9
+ @action_type.to_s.humanize
10
+ end
11
+
12
+ def validate
13
+ @errors << 'target is a required option for :attack' if target.nil?
14
+ @errors << t('validation.shove.invalid_target_size') if (target.size_identifier - @source.size_identifier) > 1
15
+ end
16
+
17
+ def build_map
18
+ OpenStruct.new({
19
+ action: self,
20
+ param: [
21
+ {
22
+ type: :select_target,
23
+ range: 5,
24
+ target_types: %i[enemies allies],
25
+ num: 1
26
+ }
27
+ ],
28
+ next: lambda { |target|
29
+ self.target = target
30
+ OpenStruct.new({
31
+ param: nil,
32
+ next: -> { self }
33
+ })
34
+ }
35
+ })
36
+ end
37
+
38
+ def self.build(session, source)
39
+ action = GrappleAction.new(session, source, :grapple)
40
+ action.build_map
41
+ end
42
+
43
+ # Build the attack roll information
44
+ # @param session [Natural20::Session]
45
+ # @param map [Natural20::BattleMap]
46
+ # @option opts battle [Natural20::Battle]
47
+ # @option opts target [Natural20::Entity]
48
+ def resolve(_session, _map, opts = {})
49
+ target = opts[:target] || @target
50
+ battle = opts[:battle]
51
+ raise 'target is a required option for :attack' if target.nil?
52
+
53
+ return if (target.size_identifier - @source.size_identifier) > 1
54
+
55
+ strength_roll = @source.athletics_check!(battle)
56
+ athletics_stats = (@target.athletics_proficient? ? @target.proficiency_bonus : 0) + @target.str_mod
57
+ acrobatics_stats = (@target.acrobatics_proficient? ? @target.proficiency_bonus : 0) + @target.dex_mod
58
+
59
+ grapple_success = false
60
+ if @target.incapacitated? || !battle.opposing?(@source, target)
61
+ grapple_success = true
62
+ else
63
+ contested_roll = if athletics_stats > acrobatics_stats
64
+ @target.athletics_check!(battle,
65
+ description: t('die_roll.contest'))
66
+ else
67
+ @target.acrobatics_check!(
68
+ opts[:battle], description: t('die_roll.contest')
69
+ )
70
+ end
71
+ grapple_success = strength_roll.result >= contested_roll.result
72
+ end
73
+
74
+ @result = if grapple_success
75
+ [{
76
+ source: @source,
77
+ target: target,
78
+ type: :grapple,
79
+ success: true,
80
+ battle: battle,
81
+ source_roll: strength_roll,
82
+ target_roll: contested_roll
83
+ }]
84
+ else
85
+ [{
86
+ source: @source,
87
+ target: target,
88
+ type: :grapple,
89
+ success: false,
90
+ battle: battle,
91
+ source_roll: strength_roll,
92
+ target_roll: contested_roll
93
+ }]
94
+ end
95
+ end
96
+
97
+ def apply!(battle)
98
+ @result.each do |item|
99
+ case (item[:type])
100
+ when :grapple
101
+ if item[:success]
102
+ item[:target].grappled_by!(@source)
103
+ Natural20::EventManager.received_event(event: :grapple_success,
104
+ target: item[:target], source: @source,
105
+ source_roll: item[:source_roll],
106
+ target_roll: item[:target_roll])
107
+ else
108
+ Natural20::EventManager.received_event(event: :grapple_failure,
109
+ target: item[:target], source: @source,
110
+ source_roll: item[:source_roll],
111
+ target_roll: item[:target_roll])
112
+ end
113
+
114
+ battle.entity_state_for(item[:source])[:action] -= 1
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ class DropGrappleAction < Natural20::Action
121
+ attr_accessor :target
122
+
123
+ def self.can?(entity, battle, _options = {})
124
+ battle.nil? || entity.grappling?
125
+ end
126
+
127
+ def to_s
128
+ @action_type.to_s.humanize
129
+ end
130
+
131
+ def build_map
132
+ OpenStruct.new({
133
+ action: self,
134
+ param: [
135
+ {
136
+ type: :select_target,
137
+ targets: @source.grappling_targets,
138
+ num: 1
139
+ }
140
+ ],
141
+ next: lambda { |target|
142
+ self.target = target
143
+ OpenStruct.new({
144
+ param: nil,
145
+ next: -> { self }
146
+ })
147
+ }
148
+ })
149
+ end
150
+
151
+ def self.build(session, source)
152
+ action = DropGrappleAction.new(session, source, :grapple)
153
+ action.build_map
154
+ end
155
+
156
+ # @param session [Natural20::Session]
157
+ # @param map [Natural20::BattleMap]
158
+ # @option opts battle [Natural20::Battle]
159
+ # @option opts target [Natural20::Entity]
160
+ def resolve(_session, _map, opts = {})
161
+ target = opts[:target] || @target
162
+ battle = opts[:battle]
163
+ @result =
164
+ [{
165
+ source: @source,
166
+ target: target,
167
+ type: :drop_grapple,
168
+ battle: battle
169
+ }]
170
+ end
171
+
172
+ def apply!(_battle)
173
+ @result.each do |item|
174
+ case (item[:type])
175
+ when :drop_grapple
176
+ item[:target].escape_grapple_from!(@source)
177
+ Natural20::EventManager.received_event(event: :drop_grapple,
178
+ target: item[:target], source: @source,
179
+ source_roll: item[:source_roll],
180
+ target_roll: item[:target_roll])
181
+
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,74 @@
1
+ # typed: true
2
+ class GroundInteractAction < Natural20::Action
3
+ attr_accessor :target, :ground_items
4
+
5
+ def self.can?(entity, battle)
6
+ battle.nil? || (entity.total_actions(battle).positive? || entity.free_object_interaction?(battle)) && items_on_the_ground_count(entity, battle).positive?
7
+ end
8
+
9
+ def self.build(session, source)
10
+ action = GroundInteractAction.new(session, source, :ground_interact)
11
+ action.build_map
12
+ end
13
+
14
+ def self.items_on_the_ground_count(entity, battle)
15
+ battle.map.items_on_the_ground(entity).inject(0) { |total, item| total + item[1].size }
16
+ end
17
+
18
+ def build_map
19
+ OpenStruct.new({
20
+ action: self,
21
+ param: [
22
+ {
23
+ type: :select_ground_items
24
+ }
25
+ ],
26
+ next: lambda { |object|
27
+ self.ground_items = object
28
+
29
+ OpenStruct.new({
30
+ param: nil,
31
+ next: lambda {
32
+ self
33
+ }
34
+ })
35
+ }
36
+ })
37
+ end
38
+
39
+ def resolve(_session, map = nil, opts = {})
40
+ battle = opts[:battle]
41
+
42
+ actions = ground_items.map do |g, items|
43
+ [g, { action: :pickup, items: items, source: @source, target: g, battle: opts[:battle] }]
44
+ end.to_h
45
+
46
+ @result << {
47
+ source: @source,
48
+ actions: actions,
49
+ map: map,
50
+ battle: battle,
51
+ type: :pickup
52
+ }
53
+
54
+ self
55
+ end
56
+
57
+ def apply!(battle)
58
+ @result.each do |item|
59
+ entity = item[:source]
60
+ case (item[:type])
61
+ when :pickup
62
+ item[:actions].each do |g, action|
63
+ g.use!(nil, action)
64
+ end
65
+
66
+ if item[:cost] == :action
67
+ battle&.consume!(entity, :action, 1)
68
+ else
69
+ battle&.consume!(entity, :free_object_interaction, 1) || battle&.consume!(entity, :action, 1)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end