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,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