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