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,189 @@
1
+ # typed: false
2
+ module ItemLibrary
3
+ class DoorObject < Object
4
+ attr_reader :state, :locked, :key_name
5
+
6
+ def opaque?
7
+ closed? && !dead?
8
+ end
9
+
10
+ def unlock!
11
+ @locked = false
12
+ end
13
+
14
+ def lock!
15
+ @locked = true
16
+ end
17
+
18
+ def locked?
19
+ @locked
20
+ end
21
+
22
+ def passable?
23
+ opened? || dead?
24
+ end
25
+
26
+ def closed?
27
+ @state == :closed
28
+ end
29
+
30
+ def opened?
31
+ @state == :opened
32
+ end
33
+
34
+ def open!
35
+ @state = :opened
36
+ end
37
+
38
+ def close!
39
+ @state = :closed
40
+ end
41
+
42
+ def token
43
+ return '`' if dead?
44
+
45
+ pos_x, pos_y = position
46
+ t = if map.wall?(pos_x - 1, pos_y) || map.wall?(pos_x + 1, pos_y)
47
+ opened? ? '-' : '='
48
+ else
49
+ opened? ? '|' : '║'
50
+ end
51
+
52
+ [t]
53
+ end
54
+
55
+ def token_opened
56
+ @properties[:token_open].presence || '-'
57
+ end
58
+
59
+ def token_closed
60
+ @properties[:token_closed].presence || '='
61
+ end
62
+
63
+ # Returns available interaction with this object
64
+ # @param entity [Natural20::PlayerCharacter]
65
+ # @return [Array]
66
+ def available_interactions(entity, battle = nil)
67
+ interaction_actions = {}
68
+ if locked?
69
+ interaction_actions[:unlock] = { disabled: !entity.item_count(:"#{key_name}").positive?,
70
+ disabled_text: t('object.door.key_required') }
71
+ if entity.item_count('thieves_tools').positive? && entity.proficient?('thieves_tools')
72
+ interaction_actions[:lockpick] =
73
+ { disabled: !entity.action?(battle), disabled_text: t('object.door.action_required') }
74
+ end
75
+ return interaction_actions
76
+ end
77
+
78
+ if opened?
79
+ { close: { disabled: someone_blocking_the_doorway?, disabled_text: t('object.door.door_blocked') } }
80
+ else
81
+ { open: {}, lock: { disabled: !entity.item_count(:"#{key_name}").positive?,
82
+ disabled_text: t('object.door.key_required') } }
83
+ end
84
+ end
85
+
86
+ def interactable?
87
+ true
88
+ end
89
+
90
+ # @param entity [Natural20::Entity]
91
+ # @param action [InteractAction]
92
+ def resolve(entity, action, _other_params, opts = {})
93
+ return if action.nil?
94
+
95
+ case action
96
+ when :open
97
+ if !locked?
98
+ {
99
+ action: action
100
+ }
101
+ else
102
+ {
103
+ action: :door_locked
104
+ }
105
+ end
106
+ when :close
107
+ {
108
+ action: action
109
+ }
110
+ when :lockpick
111
+ lock_pick_roll = entity.lockpick!(opts[:battle])
112
+
113
+ if lock_pick_roll.result >= lockpick_dc
114
+ { action: :lockpick_success, roll: lock_pick_roll, cost: :action }
115
+ else
116
+ { action: :lockpick_fail, roll: lock_pick_roll, cost: :action }
117
+ end
118
+ when :unlock
119
+ entity.item_count(:"#{key_name}").positive? ? { action: :unlock } : { action: :unlock_failed }
120
+ when :lock
121
+ entity.item_count(:"#{key_name}").positive? ? { action: :lock } : { action: :lock_failed }
122
+ end
123
+ end
124
+
125
+ # @param entity [Natural20::Entity]
126
+ # @param result [Hash]
127
+ def use!(entity, result)
128
+ case (result[:action])
129
+ when :open
130
+ open! if closed?
131
+ when :close
132
+ return unless opened?
133
+
134
+ if someone_blocking_the_doorway?
135
+ return Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
136
+ sub_type: :close_failed, result: :failed, reason: 'Cannot close door since something is in the doorway')
137
+ end
138
+
139
+ close!
140
+ when :lockpick_success
141
+ return unless locked?
142
+
143
+ unlock!
144
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
145
+ sub_type: :unlock, result: :success, lockpick: true, roll: result[:roll], reason: 'Door unlocked using lockpick.')
146
+ when :lockpick_fail
147
+ entity.deduct_item('thieves_tools')
148
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
149
+ sub_type: :unlock, result: :failed, roll: result[:roll], reason: 'Lockpicking failed and the theives tools are now broken')
150
+ when :unlock
151
+ return unless locked?
152
+
153
+ unlock!
154
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
155
+ sub_type: :unlock, result: :success, reason: t('object.door.unlock'))
156
+ when :lock
157
+ return unless unlocked?
158
+
159
+ lock!
160
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
161
+ sub_type: :lock, result: :success, reason: t('object.door.lock'))
162
+ when :door_locked
163
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
164
+ sub_type: :open_failed, result: :failed, reason: 'Cannot open door since door is locked.')
165
+ when :unlock_failed
166
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
167
+ sub_type: :unlock_failed, result: :failed, reason: 'Correct Key missing.')
168
+ end
169
+ end
170
+
171
+ def lockpick_dc
172
+ (@properties[:lockpick_dc].presence || 10)
173
+ end
174
+
175
+ protected
176
+
177
+ def someone_blocking_the_doorway?
178
+ !!map.entity_at(*position)
179
+ end
180
+
181
+ def on_take_damage(battle, damage_params); end
182
+
183
+ def setup_other_attributes
184
+ @state = @properties[:state]&.to_sym || :closed
185
+ @locked = @properties[:locked]
186
+ @key_name = @properties[:key]
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,124 @@
1
+ # typed: false
2
+ module ItemLibrary
3
+ class Ground < Object
4
+ include Natural20::Container
5
+
6
+ attr_reader :state, :locked, :key_name
7
+
8
+ # Builds a custom UI map
9
+ # @param action [Symbol] The item specific action
10
+ # @param action_object [InteractAction]
11
+ # @return [OpenStruct]
12
+ def build_map(action, action_object)
13
+ case action
14
+ when :drop
15
+ OpenStruct.new({
16
+ action: action_object,
17
+ param: [
18
+ {
19
+ type: :select_items,
20
+ label: action_object.source.items_label,
21
+ items: action_object.source.inventory
22
+ }
23
+ ],
24
+ next: lambda { |items|
25
+ action_object.other_params = items
26
+ OpenStruct.new({
27
+ param: nil,
28
+ next: lambda {
29
+ action_object
30
+ }
31
+ })
32
+ }
33
+ })
34
+ when :pickup
35
+ OpenStruct.new({
36
+ action: action_object,
37
+ param: [
38
+ {
39
+ type: :select_items,
40
+ label: items_label,
41
+ items: inventory
42
+ }
43
+ ],
44
+ next: lambda { |items|
45
+ action_object.other_params = items
46
+ OpenStruct.new({
47
+ param: nil,
48
+ next: lambda {
49
+ action_object
50
+ }
51
+ })
52
+ }
53
+ })
54
+ end
55
+ end
56
+
57
+ def opaque?
58
+ false
59
+ end
60
+
61
+ def passable?
62
+ true
63
+ end
64
+
65
+ def placeable?
66
+ true
67
+ end
68
+
69
+ def token
70
+ ["\u00B7".encode('utf-8')]
71
+ end
72
+
73
+ def color
74
+ :cyan
75
+ end
76
+
77
+ # Returns available interaction with this object
78
+ # @param entity [Natural20::PlayerCharacter]
79
+ # @return [Array]
80
+ def available_interactions(_entity, _battle = nil)
81
+ []
82
+ end
83
+
84
+ def interactable?
85
+ false
86
+ end
87
+
88
+ # @param entity [Natural20::Entity]
89
+ # @param action [InteractAction]
90
+ def resolve(entity, action, other_params, opts = {})
91
+ return if action.nil?
92
+
93
+ case action
94
+ when :drop, :pickup
95
+ { action: action, items: other_params, source: entity, target: self, battle: opts[:battle] }
96
+ end
97
+ end
98
+
99
+ # @param entity [Natural20::Entity]
100
+ # @param result [Hash]
101
+ def use!(_entity, result)
102
+ case result[:action]
103
+ when :drop
104
+ store(result[:battle], result[:source], result[:target], result[:items])
105
+ when :pickup
106
+ retrieve(result[:battle], result[:source], result[:target], result[:items])
107
+ end
108
+ end
109
+
110
+ def list_notes(_entity, _perception, highlight: false)
111
+ inventory.map do |_item|
112
+ t("object.#{m.label}", default: m.label)
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ def on_take_damage(battle, damage_params); end
119
+
120
+ def setup_other_attributes
121
+ @inventory = {}
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,51 @@
1
+ # typed: true
2
+ module ItemLibrary
3
+ class HealingPotion < BaseItem
4
+ def build_map(action)
5
+ OpenStruct.new({
6
+ param: [
7
+ {
8
+ type: :select_target,
9
+ num: 1,
10
+ range: 5,
11
+ target_types: %i[allies self]
12
+ }
13
+ ],
14
+ next: lambda { |target|
15
+ action.target = target
16
+ OpenStruct.new({
17
+ param: nil,
18
+ next: lambda {
19
+ action
20
+ }
21
+ })
22
+ }
23
+ })
24
+ end
25
+
26
+ def initialize(name, properties)
27
+ @name = name
28
+ @properties = properties
29
+ end
30
+
31
+ def consumable?
32
+ @properties[:consumable]
33
+ end
34
+
35
+ # @param entity [Natural20::Entity]
36
+ # @param battle [Natrual20::Battle]
37
+ def resolve(entity, battle)
38
+ hp_regain_roll = Natural20::DieRoll.roll(@properties[:hp_regained], description: t('dice_roll.healing_potion'),
39
+ entity: entity,
40
+ battle: battle)
41
+
42
+ {
43
+ hp_gain_roll: hp_regain_roll
44
+ }
45
+ end
46
+
47
+ def use!(entity, result)
48
+ entity.heal!(result[:hp_gain_roll].result)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,153 @@
1
+ # typed: true
2
+ module ItemLibrary
3
+ module AreaTrigger
4
+ def area_trigger_handler(_entity, _entity_pos, is_flying); end
5
+ end
6
+
7
+ class Object
8
+ class InvalidInteractionAction < StandardError
9
+ attr_reader :action, :valid_actions
10
+
11
+ # @param action [Symbol]
12
+ # @param valid_actions [Array]
13
+ def initialize(action, valid_actions = [])
14
+ @action = action
15
+ @valid_actions = valid_actions
16
+ end
17
+
18
+ def message
19
+ "Invalid action specified #{action}. should be in #{valid_actions.join(',')}"
20
+ end
21
+ end
22
+
23
+ include Natural20::Entity
24
+ include Natural20::Notable
25
+
26
+ attr_accessor :hp, :statuses, :resistances, :name, :map
27
+
28
+ def initialize(map, properties)
29
+ @name = properties[:name]
30
+ @map = map
31
+ @session = map.session
32
+ @statuses = Set.new
33
+ @properties = properties
34
+ @resistances = properties[:resistances].presence || []
35
+ setup_other_attributes
36
+ @hp = if !properties[:hp_die].blank?
37
+ Natural20::DieRoll.roll(properties[:hp_die]).result
38
+ else
39
+ properties[:max_hp]
40
+ end
41
+ if @properties[:inventory]
42
+ @inventory = @properties[:inventory].map do |inventory|
43
+ [inventory[:type].to_sym, OpenStruct.new({ qty: inventory[:qty] })]
44
+ end.to_h
45
+ end
46
+ end
47
+
48
+ def name
49
+ @properties[:label] || @name
50
+ end
51
+
52
+ def label
53
+ @properties[:label] || name
54
+ end
55
+
56
+ def position
57
+ map.position_of(self)
58
+ end
59
+
60
+ def color
61
+ @properties[:color]
62
+ end
63
+
64
+ def armor_class
65
+ @properties[:default_ac]
66
+ end
67
+
68
+ def opaque?
69
+ @properties[:opaque]
70
+ end
71
+
72
+ def half_cover?
73
+ @properties[:cover] == 'half'
74
+ end
75
+
76
+ def three_quarter_cover?
77
+ @properties[:cover] == 'three_quarter'
78
+ end
79
+
80
+ def total_cover?
81
+ @properties[:cover] == 'total'
82
+ end
83
+
84
+ def interactable?
85
+ false
86
+ end
87
+
88
+ def placeable?
89
+ @properties.key?(:placeable) ? @properties[:placeable] : true
90
+ end
91
+
92
+ def movement_cost
93
+ @properties[:movement_cost] || 1
94
+ end
95
+
96
+ def token
97
+ @properties[:token]
98
+ end
99
+
100
+ def size
101
+ @properties[:size] || :medium
102
+ end
103
+
104
+ def available_interactions(_entity, _battle)
105
+ []
106
+ end
107
+
108
+ def light_properties
109
+ @properties[:light]
110
+ end
111
+
112
+ # This terrain needs to be jumped over for movement
113
+ def jump_required?
114
+ @properties[:jump]
115
+ end
116
+
117
+ def passable?
118
+ @properties[:passable]
119
+ end
120
+
121
+ def wall?
122
+ @properties[:wall]
123
+ end
124
+
125
+ def concealed?
126
+ false
127
+ end
128
+
129
+ def describe_health
130
+ ''
131
+ end
132
+
133
+ def object?
134
+ true
135
+ end
136
+
137
+ def npc?
138
+ false
139
+ end
140
+
141
+ def pc?
142
+ false
143
+ end
144
+
145
+ def items_label
146
+ I18n.t(:"object.#{self.class}.item_label", default: "#{name} Items")
147
+ end
148
+
149
+ protected
150
+
151
+ def setup_other_attributes; end
152
+ end
153
+ end