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,288 @@
1
+ # typed: true
2
+ module Natural20
3
+ class EventManager
4
+ class EventLogger
5
+ # @param log_level [Integer,String]
6
+ def initialize(log_level)
7
+ @log_level = log_level.to_i
8
+ end
9
+
10
+ def debug(message = '')
11
+ puts message if @log_level < 1
12
+ end
13
+ end
14
+
15
+ def self.clear
16
+ @event_listeners = {}
17
+ end
18
+
19
+ # Registers an event listener
20
+ # @param events [Symbol,Array<Symbol>] list of event types
21
+ # @param callable [Proc] handler
22
+ def self.register_event_listener(events, callable)
23
+ events = [events] unless events.is_a?(Array)
24
+ @event_listeners ||= {}
25
+ events.each do |event|
26
+ @event_listeners[event] ||= []
27
+ @event_listeners[event] << callable unless @event_listeners[event].include?(callable)
28
+ end
29
+ end
30
+
31
+ def self.received_event(event)
32
+ return if @event_listeners.nil?
33
+
34
+ @event_listeners[event[:event]]&.each do |callable|
35
+ callable.call(event)
36
+ end
37
+ end
38
+
39
+ # Sets the current player so that event text can be customized
40
+ # @param battle [Natural20::Battle] Current battle
41
+ # @param entites [Array] An array of entities
42
+ def self.set_context(battle, entities = [])
43
+ @battle = battle
44
+ @current_entity_context = entities
45
+ end
46
+
47
+ # @return [EventLogger]
48
+ def self.logger
49
+ @logger ||= EventLogger.new(ENV['NAT20_LOG_LEVEL'].presence || '1')
50
+ @logger
51
+ end
52
+
53
+ def self.standard_cli
54
+ Natural20::EventManager.clear
55
+ event_handlers = { died: lambda { |event|
56
+ puts "#{show_name(event)} died."
57
+ },
58
+ unconscious: lambda { |event|
59
+ puts "#{show_name(event)} unconscious."
60
+ },
61
+ attacked: lambda { |event|
62
+ advantage_mod = event[:advantage_mod]
63
+ advantage, disadvantage = event[:adv_info]
64
+ sneak = event[:sneak_attack] ? (event[:sneak_attack]).to_s : nil
65
+ damage = (event[:damage_roll]).to_s
66
+ damage_str = "#{[damage,
67
+ sneak].compact.join(' + sneak ')} = #{event[:value]} #{event[:damage_type]}"
68
+ if event[:cover_ac].try(:positive?)
69
+ cover_str = " (behind cover +#{event[:cover_ac]} ac)"
70
+ end
71
+
72
+ advantage_str = if advantage_mod&.positive?
73
+ ' with advantage'.colorize(:green)
74
+ elsif advantage_mod&.negative?
75
+ ' with disadvantage'.colorize(:red)
76
+ else
77
+ ''
78
+ end
79
+ str_token = event[:attack_roll] ? 'event.attack' : 'event.attack_no_roll'
80
+ puts t(str_token, opportunity: event[:as_reaction] ? 'Opportunity Attack: ' : '',
81
+ source: show_name(event),
82
+ target: "#{event[:target].name}#{cover_str}",
83
+ attack_name: event[:attack_name],
84
+ advantage: advantage_str,
85
+ attack_roll: event[:attack_roll].to_s.colorize(:green),
86
+ attack_value: event[:attack_roll]&.result,
87
+ damage: damage_str)
88
+ },
89
+ damage: lambda { |event|
90
+ puts "#{show_name(event)} #{event[:source].describe_health}"
91
+ },
92
+ miss: lambda { |event|
93
+ advantage_mod = event[:advantage_mod]
94
+ advantage_str = if advantage_mod.positive?
95
+ ' with advantage'.colorize(:green)
96
+ elsif advantage_mod.negative?
97
+ ' with disadvantage'.colorize(:red)
98
+ else
99
+ ''
100
+ end
101
+ puts "#{event[:as_reaction] ? 'Opportunity Attack: ' : ''} rolled #{advantage_str} #{event[:attack_roll]} ... #{event[:source].name&.colorize(:blue)} missed his attack #{event[:attack_name].colorize(:red)} on #{event[:target].name.colorize(:green)}"
102
+ },
103
+ initiative: lambda { |event|
104
+ puts "#{show_name(event)} rolled a #{event[:roll]} = (#{event[:value]}) with dex tie break for initiative."
105
+ },
106
+ move: lambda { |event|
107
+ puts "#{show_name(event)} moved #{(event[:path].size - 1) * event[:feet_per_grid]}ft."
108
+ },
109
+ dodge: lambda { |event|
110
+ puts t('event.dodge', name: show_name(event))
111
+ },
112
+ help: lambda { |event|
113
+ puts "#{show_name(event)} is helping to attack #{event[:target].name&.colorize(:red)}"
114
+ },
115
+ second_wind: lambda { |event|
116
+ puts SecondWindAction.describe(event)
117
+ },
118
+ heal: lambda { |event|
119
+ puts "#{show_name(event)} heals for #{event[:value]}hp"
120
+ },
121
+ hit_die: lambda { |event|
122
+ puts t('event.hit_die', source: show_name(event), roll: event[:roll].to_s,
123
+ value: event[:roll].result)
124
+ },
125
+ object_interaction: lambda { |event|
126
+ if event[:roll]
127
+ puts "#{event[:roll]} = #{event[:roll].result} -> #{event[:reason]}"
128
+ else
129
+ puts (event[:reason]).to_s
130
+ end
131
+ },
132
+ perception: lambda { |event|
133
+ puts "#{show_name(event)} rolls #{event[:perception_roll]} on perception"
134
+ },
135
+
136
+ death_save: lambda { |event|
137
+ puts t('event.death_save', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
138
+ saves: event[:saves], fails: event[:fails]).colorize(:blue)
139
+ },
140
+
141
+ death_fail: lambda { |event|
142
+ if event[:roll]
143
+ puts t('event.death_fail', name: show_name(event), roll: event[:roll].to_s, value: event[:roll].result,
144
+ saves: event[:saves], fails: event[:fails]).colorize(:red)
145
+ else
146
+ puts t('event.death_fail_hit', name: show_name(event), saves: event[:saves],
147
+ fails: event[:fails]).colorize(:red)
148
+ end
149
+ },
150
+ great_weapon_fighting_roll: lambda { |event|
151
+ puts t('event.great_weapon_fighting_roll', name: show_name(event),
152
+ roll: event[:roll], prev_roll: event[:prev_roll])
153
+ },
154
+ feature_protection: lambda { |event|
155
+ puts t('event.feature_protection', source: event[:source]&.name,
156
+ target: event[:target]&.name, attacker: event[:attacker]&.name)
157
+ },
158
+ prone: lambda { |event|
159
+ puts t('event.status.prone', name: show_name(event))
160
+ },
161
+
162
+ stand: lambda { |event|
163
+ puts t('event.status.stand', name: show_name(event))
164
+ },
165
+
166
+ %i[acrobatics athletics] => lambda { |event|
167
+ if event[:success]
168
+ puts t("event.#{event[:event]}.success", name: show_name(event), roll: event[:roll],
169
+ value: event[:roll].result)
170
+ else
171
+ puts t("event.#{event[:event]}.failure", name: show_name(event), roll: event[:roll],
172
+ value: event[:roll].result)
173
+ end
174
+ },
175
+
176
+ start_of_combat: lambda { |event|
177
+ puts t('event.combat_start')
178
+ event[:combat_order].each_with_index do |entity_and_initiative, index|
179
+ entity, initiative = entity_and_initiative
180
+ puts "#{index + 1}. #{decorate_name(entity)} #{initiative}"
181
+ end
182
+ },
183
+
184
+ end_of_combat: lambda { |_event|
185
+ puts t('event.combat_end')
186
+ },
187
+ grapple_success: lambda { |event|
188
+ if event[:target_roll]
189
+ puts t('event.grapple_success',
190
+ source: show_name(event), target: event[:target]&.name,
191
+ source_roll: event[:source_roll],
192
+ source_roll_value: event[:source_roll].result,
193
+ target_roll: event[:target_roll],
194
+ target_roll_value: event[:target_roll].result)
195
+ else
196
+ puts t('event.grapple_success_no_roll',
197
+ source: show_name(event), target: event[:target]&.name)
198
+ end
199
+ },
200
+ first_aid: lambda { |event|
201
+ puts t('event.first_aid', name: show_name(event),
202
+ target: event[:target]&.name, roll: event[:roll], value: event[:roll].result)
203
+ },
204
+ first_aid_failure: lambda { |event|
205
+ puts t('event.first_aid_failure', name: show_name(event),
206
+ target: event[:target]&.name, roll: event[:roll], value: event[:roll].result)
207
+ },
208
+ grapple_failure: lambda { |event|
209
+ puts t('event.grapple_failure',
210
+ source: show_name(event), target: event[:target]&.name,
211
+ source_roll: event[:source_roll],
212
+ source_roll_value: event[:source_roll].result,
213
+ target_roll: event[:target_roll],
214
+ target_roll_value: event[:target_roll_value].result)
215
+ },
216
+ drop_grapple: lambda { |event|
217
+ puts t('event.drop_grapple',
218
+ source: show_name(event), target: event[:target]&.name)
219
+ },
220
+ flavor: lambda { |event|
221
+ puts t("event.flavor.#{event[:text]}", source: event[:source]&.name,
222
+ target: event[:target]&.name)
223
+ },
224
+ shove_success: lambda do |event|
225
+ opts = {
226
+ source: event[:source]&.name, target: event[:target]&.name,
227
+ source_roll: event[:source_roll],
228
+ source_roll_value: event[:source_roll].result,
229
+ target_roll: event[:target_roll],
230
+ target_roll_value: event[:target_roll].result
231
+ }
232
+ if event[:knock_prone]
233
+ puts t('event.knock_prone_success', opts)
234
+ else
235
+ puts t('event.shove_success', opts)
236
+ end
237
+ end,
238
+ shove_failure: lambda do |event|
239
+ opts = {
240
+ source: event[:source]&.name, target: event[:target]&.name,
241
+ source_roll: event[:source_roll],
242
+ source_roll_value: event[:source_roll].result,
243
+ target_roll: event[:target_roll],
244
+ target_roll_value: event[:target_roll].result
245
+ }
246
+ if event[:knock_prone]
247
+ puts t('event.knock_prone_failure', opts)
248
+ else
249
+ puts t('event.shove_failure', opts)
250
+ end
251
+ end,
252
+ %i[escape_grapple_success escape_grapple_failure] => lambda { |event|
253
+ puts t("event.#{event[:event]}",
254
+ source: show_name(event), target: event[:target]&.name,
255
+ source_roll: event[:source_roll],
256
+ source_roll_value: event[:source_roll].result,
257
+ target_roll: event[:target_roll],
258
+ target_roll_value: event[:target_roll].result)
259
+ } }
260
+ event_handlers.each do |k, v|
261
+ EventManager.register_event_listener(k, v)
262
+ end
263
+ end
264
+
265
+ # @param event [Hash]
266
+ def self.show_name(event)
267
+ decorate_name(event[:source])
268
+ end
269
+
270
+ # @param event [Hash]
271
+ def self.decorate_name(entity)
272
+ if @battle && @current_entity_context && entity
273
+
274
+ seen_attacker = @current_entity_context.detect do |c|
275
+ c == entity || @battle.map.can_see?(c, entity)
276
+ end
277
+ color = @battle.entity_group_for(seen_attacker) != @battle.entity_group_for(entity) ? :red : :blue
278
+ seen_attacker ? entity.name&.colorize(color) : I18n.t('event.something')
279
+ else
280
+ entity.name&.colorize(:blue)
281
+ end
282
+ end
283
+
284
+ def self.t(token, options = {})
285
+ I18n.t(token, options)
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,27 @@
1
+ # typed: true
2
+ module ItemLibrary
3
+ class BaseItem
4
+ attr_reader :name
5
+
6
+ def initialize(name, properties)
7
+ @name = name
8
+ @properties = properties
9
+ end
10
+
11
+ def consumable?
12
+ !!@properties[:consumable]
13
+ end
14
+
15
+ def item?
16
+ true
17
+ end
18
+
19
+ def use!(battle, map, entity); end
20
+
21
+ protected
22
+
23
+ def t(key, options = {})
24
+ I18n.t(key, options)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,230 @@
1
+ # typed: false
2
+ module ItemLibrary
3
+ class Chest < 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 :store
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 :loot
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 unlock!
62
+ @locked = false
63
+ end
64
+
65
+ def lock!
66
+ @locked = true
67
+ end
68
+
69
+ def locked?
70
+ @locked
71
+ end
72
+
73
+ def passable?
74
+ true
75
+ end
76
+
77
+ def closed?
78
+ @state == :closed
79
+ end
80
+
81
+ def opened?
82
+ @state == :opened
83
+ end
84
+
85
+ def open!
86
+ @state = :opened
87
+ end
88
+
89
+ def close!
90
+ @state = :closed
91
+ end
92
+
93
+ def token
94
+ return '`' if dead?
95
+
96
+ t = opened? ? "\u2610" : "\u2610"
97
+ [t]
98
+ end
99
+
100
+ def color
101
+ opened? ? :white : super
102
+ end
103
+
104
+ # Returns available interaction with this object
105
+ # @param entity [Natural20::PlayerCharacter]
106
+ # @return [Array]
107
+ def available_interactions(entity, battle = nil)
108
+ interaction_actions = {}
109
+ if locked?
110
+ interaction_actions[:unlock] = { disabled: !entity.item_count(:"#{key_name}").positive?,
111
+ disabled_text: t('object.door.key_required') }
112
+ if entity.item_count('thieves_tools').positive? && entity.proficient?('thieves_tools')
113
+ interaction_actions[:lockpick] =
114
+ { disabled: !entity.action?(battle), disabled_text: t('object.door.action_required') }
115
+ end
116
+ return interaction_actions
117
+ end
118
+
119
+ if opened?
120
+ %i[close store loot]
121
+ else
122
+ { open: {}, lock: { disabled: !entity.item_count(:"#{key_name}").positive?,
123
+ disabled_text: t('object.door.key_required') } }
124
+ end
125
+ end
126
+
127
+ def interactable?
128
+ true
129
+ end
130
+
131
+ # @param entity [Natural20::Entity]
132
+ # @param action [InteractAction]
133
+ def resolve(entity, action, other_params, opts = {})
134
+ return if action.nil?
135
+
136
+ case action
137
+ when :open
138
+ if !locked?
139
+ {
140
+ action: action
141
+ }
142
+ else
143
+ {
144
+ action: :door_locked
145
+ }
146
+ end
147
+ when :loot, :store
148
+ { action: action, items: other_params, source: entity, target: self, battle: opts[:battle] }
149
+ when :close
150
+ {
151
+ action: action
152
+ }
153
+ when :lockpick
154
+ lock_pick_roll = entity.lockpick!(opts[:battle])
155
+
156
+ if lock_pick_roll.result >= lockpick_dc
157
+ { action: :lockpick_success, roll: lock_pick_roll, cost: :action }
158
+ else
159
+ { action: :lockpick_fail, roll: lock_pick_roll, cost: :action }
160
+ end
161
+ when :unlock
162
+ entity.item_count(:"#{key_name}").positive? ? { action: :unlock } : { action: :unlock_failed }
163
+ when :lock
164
+ entity.item_count(:"#{key_name}").positive? ? { action: :lock } : { action: :lock_failed }
165
+ end
166
+ end
167
+
168
+ # @param entity [Natural20::Entity]
169
+ # @param result [Hash]
170
+ def use!(entity, result)
171
+ case result[:action]
172
+ when :store
173
+ store(result[:battle], result[:source], result[:target], result[:items])
174
+ when :loot
175
+ retrieve(result[:battle], result[:source], result[:target], result[:items])
176
+ when :open
177
+ open! if closed?
178
+ when :close
179
+ return unless opened?
180
+
181
+ close!
182
+ when :lockpick_success
183
+ return unless locked?
184
+
185
+ unlock!
186
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
187
+ sub_type: :unlock, result: :success, lockpick: true, roll: result[:roll], reason: t(:"object.chest.unlock"))
188
+ when :lockpick_fail
189
+ return unless locked?
190
+
191
+ entity.deduct_item('thieves_tools')
192
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
193
+ sub_type: :unlock, result: :failed, roll: result[:roll], reason: t('object.lockpick_failed'))
194
+
195
+ when :unlock
196
+ return unless locked?
197
+
198
+ unlock!
199
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
200
+ sub_type: :unlock, result: :success, reason: t(:"object.chest.unlock"))
201
+ when :lock
202
+ return unless unlocked?
203
+
204
+ lock!
205
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
206
+ sub_type: :lock, result: :success, reason: t(:"object.chest.lock"))
207
+ when :door_locked
208
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
209
+ sub_type: :open_failed, result: :failed, reason: 'Cannot open chest since chest is locked.')
210
+ when :unlock_failed
211
+ Natural20::EventManager.received_event(source: self, user: entity, event: :object_interaction,
212
+ sub_type: :unlock_failed, result: :failed, reason: 'Correct Key missing.')
213
+ end
214
+ end
215
+
216
+ def lockpick_dc
217
+ (@properties[:lockpick_dc].presence || 10)
218
+ end
219
+
220
+ protected
221
+
222
+ def on_take_damage(battle, damage_params); end
223
+
224
+ def setup_other_attributes
225
+ @state = @properties[:state]&.to_sym || :closed
226
+ @locked = @properties[:locked]
227
+ @key_name = @properties[:key]
228
+ end
229
+ end
230
+ end