natural_20 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/compute_lights +19 -0
- data/bin/console +19 -0
- data/bin/nat20 +135 -0
- data/bin/nat20.cmd +3 -0
- data/bin/nat20author +104 -0
- data/bin/setup +8 -0
- data/char_classes/fighter.yml +45 -0
- data/char_classes/rogue.yml +54 -0
- data/characters/halfling_rogue.yml +46 -0
- data/characters/high_elf_fighter.yml +49 -0
- data/fixtures/battle_sim.yml +58 -0
- data/fixtures/battle_sim_2.yml +30 -0
- data/fixtures/battle_sim_3.yml +26 -0
- data/fixtures/battle_sim_4.yml +26 -0
- data/fixtures/battle_sim_objects.yml +101 -0
- data/fixtures/corridors.yml +24 -0
- data/fixtures/elf_rogue.yml +39 -0
- data/fixtures/halfling_rogue.yml +41 -0
- data/fixtures/high_elf_fighter.yml +49 -0
- data/fixtures/human_fighter.yml +48 -0
- data/fixtures/path_finding_test.yml +11 -0
- data/fixtures/path_finding_test_2.yml +15 -0
- data/fixtures/path_finding_test_3.yml +26 -0
- data/fixtures/thin_walls.yml +53 -0
- data/fixtures/traps.yml +25 -0
- data/game.yml +20 -0
- data/items/equipment.yml +101 -0
- data/items/objects.yml +73 -0
- data/items/weapons.yml +297 -0
- data/lib/natural_20.rb +68 -0
- data/lib/natural_20/actions/action.rb +40 -0
- data/lib/natural_20/actions/attack_action.rb +372 -0
- data/lib/natural_20/actions/concerns/action_damage.rb +14 -0
- data/lib/natural_20/actions/dash_action.rb +46 -0
- data/lib/natural_20/actions/disengage_action.rb +53 -0
- data/lib/natural_20/actions/dodge_action.rb +45 -0
- data/lib/natural_20/actions/escape_grapple_action.rb +97 -0
- data/lib/natural_20/actions/first_aid_action.rb +109 -0
- data/lib/natural_20/actions/grapple_action.rb +185 -0
- data/lib/natural_20/actions/ground_interact_action.rb +74 -0
- data/lib/natural_20/actions/help_action.rb +56 -0
- data/lib/natural_20/actions/hide_action.rb +53 -0
- data/lib/natural_20/actions/interact_action.rb +91 -0
- data/lib/natural_20/actions/inventory_action.rb +23 -0
- data/lib/natural_20/actions/look_action.rb +63 -0
- data/lib/natural_20/actions/move_action.rb +254 -0
- data/lib/natural_20/actions/multiattack_action.rb +41 -0
- data/lib/natural_20/actions/prone_action.rb +38 -0
- data/lib/natural_20/actions/short_rest_action.rb +53 -0
- data/lib/natural_20/actions/shove_action.rb +142 -0
- data/lib/natural_20/actions/stand_action.rb +47 -0
- data/lib/natural_20/actions/use_item_action.rb +57 -0
- data/lib/natural_20/ai_controller/path_compute.rb +140 -0
- data/lib/natural_20/ai_controller/standard.rb +288 -0
- data/lib/natural_20/battle.rb +544 -0
- data/lib/natural_20/battle_map.rb +843 -0
- data/lib/natural_20/cli/builder/fighter_builder.rb +104 -0
- data/lib/natural_20/cli/builder/rogue_builder.rb +62 -0
- data/lib/natural_20/cli/character_builder.rb +210 -0
- data/lib/natural_20/cli/commandline_ui.rb +612 -0
- data/lib/natural_20/cli/inventory_ui.rb +136 -0
- data/lib/natural_20/cli/map_renderer.rb +165 -0
- data/lib/natural_20/concerns/container.rb +32 -0
- data/lib/natural_20/concerns/entity.rb +1213 -0
- data/lib/natural_20/concerns/evaluator/entity_state_evaluator.rb +59 -0
- data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +51 -0
- data/lib/natural_20/concerns/fighter_class.rb +35 -0
- data/lib/natural_20/concerns/health_flavor.rb +27 -0
- data/lib/natural_20/concerns/lootable.rb +94 -0
- data/lib/natural_20/concerns/movement_helper.rb +195 -0
- data/lib/natural_20/concerns/multiattack.rb +54 -0
- data/lib/natural_20/concerns/navigation.rb +87 -0
- data/lib/natural_20/concerns/notable.rb +37 -0
- data/lib/natural_20/concerns/rogue_class.rb +26 -0
- data/lib/natural_20/controller.rb +11 -0
- data/lib/natural_20/die_roll.rb +331 -0
- data/lib/natural_20/event_manager.rb +288 -0
- data/lib/natural_20/item_library/base_item.rb +27 -0
- data/lib/natural_20/item_library/chest.rb +230 -0
- data/lib/natural_20/item_library/door_object.rb +189 -0
- data/lib/natural_20/item_library/ground.rb +124 -0
- data/lib/natural_20/item_library/healing_potion.rb +51 -0
- data/lib/natural_20/item_library/object.rb +153 -0
- data/lib/natural_20/item_library/pit_trap.rb +69 -0
- data/lib/natural_20/item_library/stone_wall.rb +18 -0
- data/lib/natural_20/npc.rb +173 -0
- data/lib/natural_20/player_character.rb +414 -0
- data/lib/natural_20/session.rb +168 -0
- data/lib/natural_20/utils/cover.rb +35 -0
- data/lib/natural_20/utils/ray_tracer.rb +90 -0
- data/lib/natural_20/utils/static_light_builder.rb +72 -0
- data/lib/natural_20/utils/weapons.rb +78 -0
- data/lib/natural_20/version.rb +4 -0
- data/locales/en.yml +304 -0
- data/maps/game_map.yml +168 -0
- data/natural_20.gemspec +46 -0
- data/npcs/goblin.yml +64 -0
- data/npcs/human_guard.yml +48 -0
- data/npcs/ogre.yml +61 -0
- data/npcs/owlbear.yml +55 -0
- data/npcs/wolf.yml +46 -0
- data/races/elf.yml +44 -0
- data/races/halfling.yml +22 -0
- data/races/human.yml +13 -0
- 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
|