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,372 @@
|
|
1
|
+
# typed: true
|
2
|
+
class AttackAction < Natural20::Action
|
3
|
+
include Natural20::Cover
|
4
|
+
include Natural20::Weapons
|
5
|
+
include Natural20::ActionDamage
|
6
|
+
|
7
|
+
attr_accessor :target, :using, :npc_action, :as_reaction, :thrown, :second_hand
|
8
|
+
attr_reader :advantage_mod
|
9
|
+
|
10
|
+
# @param entity [Natural20::Entity]
|
11
|
+
# @param battle [Natural20::Battle]
|
12
|
+
# @return [Boolean]
|
13
|
+
def self.can?(entity, battle, options = {})
|
14
|
+
battle.nil? || entity.total_actions(battle).positive? || (options[:opportunity_attack] && entity.total_reactions(battle).positive?) || entity.multiattack?(
|
15
|
+
battle, options[:npc_action]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
@action_type.to_s.humanize
|
21
|
+
end
|
22
|
+
|
23
|
+
def label
|
24
|
+
if @npc_action
|
25
|
+
t('action.npc_action', name: @action_type.to_s.humanize, action_name: npc_action[:name])
|
26
|
+
else
|
27
|
+
weapon = session.load_weapon(@opts[:using] || @using)
|
28
|
+
attack_mod = @source.attack_roll_mod(weapon)
|
29
|
+
|
30
|
+
i18n_token = thrown ? 'action.attack_action_throw' : 'action.attack_action'
|
31
|
+
|
32
|
+
t(i18n_token, name: @action_type.to_s.humanize, weapon_name: weapon[:name], mod: attack_mod,
|
33
|
+
dmg: damage_modifier(@source, weapon, second_hand: second_hand))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_map
|
38
|
+
OpenStruct.new({
|
39
|
+
action: self,
|
40
|
+
param: [
|
41
|
+
{
|
42
|
+
type: :select_target,
|
43
|
+
num: 1,
|
44
|
+
weapon: using
|
45
|
+
}
|
46
|
+
],
|
47
|
+
next: lambda { |target|
|
48
|
+
self.target = target
|
49
|
+
OpenStruct.new({
|
50
|
+
param: [
|
51
|
+
{ type: :select_weapon }
|
52
|
+
],
|
53
|
+
next: lambda { |weapon|
|
54
|
+
self.using = weapon
|
55
|
+
OpenStruct.new({
|
56
|
+
param: nil,
|
57
|
+
next: -> { self }
|
58
|
+
})
|
59
|
+
}
|
60
|
+
})
|
61
|
+
}
|
62
|
+
})
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.build(session, source)
|
66
|
+
action = AttackAction.new(session, source, :attack)
|
67
|
+
action.build_map
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param battle [Natural20::Battle]
|
71
|
+
def apply!(battle)
|
72
|
+
@result.each do |item|
|
73
|
+
if item[:flavor]
|
74
|
+
Natural20::EventManager.received_event({ event: :flavor, source: item[:source], target: item[:target],
|
75
|
+
text: item[:flavor] })
|
76
|
+
end
|
77
|
+
case (item[:type])
|
78
|
+
when :prone
|
79
|
+
item[:source].prone!
|
80
|
+
when :damage
|
81
|
+
damage_event(item, battle)
|
82
|
+
when :miss
|
83
|
+
Natural20::EventManager.received_event({ attack_roll: item[:attack_roll],
|
84
|
+
attack_name: item[:attack_name],
|
85
|
+
advantage_mod: item[:advantage_mod],
|
86
|
+
as_reaction: !!as_reaction,
|
87
|
+
adv_info: item[:adv_info],
|
88
|
+
source: item[:source], target: item[:target], event: :miss })
|
89
|
+
end
|
90
|
+
|
91
|
+
# handle ammo
|
92
|
+
item[:source].deduct_item(item[:ammo], 1) if item[:ammo]
|
93
|
+
|
94
|
+
# hanle thrown items
|
95
|
+
if item[:thrown]
|
96
|
+
if item[:source].item_count(item[:weapon]).positive?
|
97
|
+
item[:source].deduct_item(item[:weapon], 1)
|
98
|
+
else
|
99
|
+
item[:source].unequip(item[:weapon], transfer_inventory: false)
|
100
|
+
end
|
101
|
+
|
102
|
+
if item[:type] == :damage
|
103
|
+
item[:target].add_item(item[:weapon])
|
104
|
+
else
|
105
|
+
ground_pos = item[:battle].map.entity_or_object_pos(item[:target])
|
106
|
+
ground_object = item[:battle].map.objects_at(*ground_pos).detect { |o| o.is_a?(ItemLibrary::Ground) }
|
107
|
+
ground_object&.add_item(item[:weapon])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if as_reaction
|
112
|
+
battle.entity_state_for(item[:source])[:reaction] -= 1
|
113
|
+
elsif item[:second_hand]
|
114
|
+
battle.entity_state_for(item[:source])[:bonus_action] -= 1
|
115
|
+
else
|
116
|
+
battle.entity_state_for(item[:source])[:action] -= 1
|
117
|
+
end
|
118
|
+
|
119
|
+
item[:source].break_stealth!(battle)
|
120
|
+
|
121
|
+
# handle two-weapon fighting
|
122
|
+
weapon = session.load_weapon(item[:weapon]) if item[:weapon]
|
123
|
+
|
124
|
+
if weapon && weapon[:properties]&.include?('light') && !battle.two_weapon_attack?(item[:source]) && !item[:second_hand]
|
125
|
+
battle.entity_state_for(item[:source])[:two_weapon] = item[:weapon]
|
126
|
+
else
|
127
|
+
battle.entity_state_for(item[:source])[:two_weapon] = nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# handle multiattacks
|
131
|
+
battle.entity_state_for(item[:source])[:multiattack]&.each do |_group, attacks|
|
132
|
+
if attacks.include?(item[:attack_name])
|
133
|
+
attacks.delete(item[:attack_name])
|
134
|
+
item[:source].clear_multiattack!(battle) if attacks.empty?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# dismiss help actions
|
139
|
+
battle.dismiss_help_for(item[:target])
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def with_advantage?
|
144
|
+
@advantage_mod.positive?
|
145
|
+
end
|
146
|
+
|
147
|
+
def with_disadvantage?
|
148
|
+
@advantage_mod.negative?
|
149
|
+
end
|
150
|
+
|
151
|
+
# Build the attack roll information
|
152
|
+
# @param session [Natural20::Session]
|
153
|
+
# @param map [Natural20::BattleMap]
|
154
|
+
# @option opts battle [Natural20::Battle]
|
155
|
+
# @option opts target [Natural20::Entity]
|
156
|
+
def resolve(_session, map, opts = {})
|
157
|
+
@result.clear
|
158
|
+
target = opts[:target] || @target
|
159
|
+
raise 'target is a required option for :attack' if target.nil?
|
160
|
+
|
161
|
+
npc_action = opts[:npc_action] || @npc_action
|
162
|
+
battle = opts[:battle]
|
163
|
+
using = opts[:using] || @using
|
164
|
+
raise 'using or npc_action is a required option for :attack' if using.nil? && npc_action.nil?
|
165
|
+
|
166
|
+
attack_name = nil
|
167
|
+
damage_roll = nil
|
168
|
+
sneak_attack_roll = nil
|
169
|
+
ammo_type = nil
|
170
|
+
|
171
|
+
npc_action = @source.npc_actions.detect { |a| a[:name].downcase == using.downcase } if @source.npc? && using
|
172
|
+
|
173
|
+
if npc_action
|
174
|
+
weapon = npc_action
|
175
|
+
attack_name = npc_action[:name]
|
176
|
+
attack_mod = npc_action[:attack]
|
177
|
+
damage_roll = npc_action[:damage_die]
|
178
|
+
ammo_type = npc_action[:ammo]
|
179
|
+
else
|
180
|
+
weapon = session.load_weapon(using.to_sym)
|
181
|
+
attack_name = weapon[:name]
|
182
|
+
ammo_type = weapon[:ammo]
|
183
|
+
attack_mod = @source.attack_roll_mod(weapon)
|
184
|
+
damage_roll = damage_modifier(@source, weapon, second_hand: second_hand)
|
185
|
+
end
|
186
|
+
|
187
|
+
# DnD 5e advantage/disadvantage checks
|
188
|
+
@advantage_mod, adv_info = target_advantage_condition(battle, @source, target, weapon)
|
189
|
+
|
190
|
+
# determine eligibility for the 'Protection' fighting style
|
191
|
+
evaluate_feature_protection(battle, map, target, adv_info) if map
|
192
|
+
|
193
|
+
# perform the dice rolls
|
194
|
+
attack_roll = Natural20::DieRoll.roll("1d20+#{attack_mod}", disadvantage: with_disadvantage?,
|
195
|
+
advantage: with_advantage?,
|
196
|
+
description: t('dice_roll.attack'), entity: @source, battle: battle)
|
197
|
+
|
198
|
+
# handle the lucky feat
|
199
|
+
attack_roll = attack_roll.reroll(lucky: true) if @source.class_feature?('lucky') && attack_roll.nat_1?
|
200
|
+
|
201
|
+
if @source.class_feature?('sneak_attack') && (weapon[:properties]&.include?('finesse') || weapon[:type] == 'ranged_attack') && (with_advantage? || battle.enemy_in_melee_range?(
|
202
|
+
target, [@source]
|
203
|
+
))
|
204
|
+
sneak_attack_roll = Natural20::DieRoll.roll(@source.sneak_attack_level, crit: attack_roll.nat_20?,
|
205
|
+
description: t('dice_roll.sneak_attack'), entity: @source, battle: battle)
|
206
|
+
end
|
207
|
+
|
208
|
+
damage = Natural20::DieRoll.roll(damage_roll, crit: attack_roll.nat_20?, description: t('dice_roll.damage'),
|
209
|
+
entity: @source, battle: battle)
|
210
|
+
|
211
|
+
if @source.class_feature?('great_weapon_fighting') && (weapon[:properties]&.include?('two_handed') || (weapon[:properties]&.include?('versatile') && entity.used_hand_slots <= 1.0))
|
212
|
+
damage.rolls.map do |roll|
|
213
|
+
if [1, 2].include?(roll)
|
214
|
+
r = Natural20::DieRoll.roll("1d#{damage.die_sides}", description: t('dice_roll.great_weapon_fighting_reroll'),
|
215
|
+
entity: @source, battle: battle)
|
216
|
+
Natural20::EventManager.received_event({ roll: r, prev_roll: roll,
|
217
|
+
source: item[:source], event: :great_weapon_fighting_roll })
|
218
|
+
r.result
|
219
|
+
else
|
220
|
+
roll
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# apply weapon bonus attacks
|
226
|
+
damage = check_weapon_bonuses(battle, weapon, damage, attack_roll)
|
227
|
+
|
228
|
+
cover_ac_adjustments = 0
|
229
|
+
hit = if attack_roll.nat_20?
|
230
|
+
true
|
231
|
+
elsif attack_roll.nat_1?
|
232
|
+
false
|
233
|
+
else
|
234
|
+
cover_ac_adjustments = calculate_cover_ac(battle.map, target) if battle.map
|
235
|
+
attack_roll.result >= (target.armor_class + cover_ac_adjustments)
|
236
|
+
end
|
237
|
+
|
238
|
+
if hit
|
239
|
+
@result << {
|
240
|
+
source: @source,
|
241
|
+
target: target,
|
242
|
+
type: :damage,
|
243
|
+
thrown: thrown,
|
244
|
+
weapon: using,
|
245
|
+
battle: battle,
|
246
|
+
advantage_mod: @advantage_mod,
|
247
|
+
damage_roll: damage_roll,
|
248
|
+
attack_name: attack_name,
|
249
|
+
attack_roll: attack_roll,
|
250
|
+
sneak_attack: sneak_attack_roll,
|
251
|
+
target_ac: target.armor_class,
|
252
|
+
cover_ac: cover_ac_adjustments,
|
253
|
+
adv_info: adv_info,
|
254
|
+
hit?: hit,
|
255
|
+
damage_type: weapon[:damage_type],
|
256
|
+
damage: damage,
|
257
|
+
ammo: ammo_type,
|
258
|
+
as_reaction: !!as_reaction,
|
259
|
+
second_hand: second_hand,
|
260
|
+
npc_action: npc_action
|
261
|
+
}
|
262
|
+
unless weapon[:on_hit].blank?
|
263
|
+
weapon[:on_hit].each do |effect|
|
264
|
+
next if effect[:if] && !@source.eval_if(effect[:if], weapon: weapon, target: target)
|
265
|
+
|
266
|
+
if effect[:save_dc]
|
267
|
+
save_type, dc = effect[:save_dc].split(':')
|
268
|
+
raise 'invalid values: save_dc should be of the form <save>:<dc>' if save_type.blank? || dc.blank?
|
269
|
+
raise 'invalid save type' unless Natural20::Entity::ATTRIBUTE_TYPES.include?(save_type)
|
270
|
+
|
271
|
+
save_roll = target.saving_throw!(save_type, battle: battle)
|
272
|
+
if save_roll.result >= dc.to_i
|
273
|
+
if effect[:success]
|
274
|
+
@result << target.apply_effect(effect[:success], battle: battle,
|
275
|
+
flavor: effect[:flavor_success])
|
276
|
+
end
|
277
|
+
elsif effect[:fail]
|
278
|
+
@result << target.apply_effect(effect[:fail], battle: battle, flavor: effect[:flavor_fail])
|
279
|
+
end
|
280
|
+
else
|
281
|
+
target.apply_effect(effect[:effect])
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
else
|
286
|
+
@result << {
|
287
|
+
attack_name: attack_name,
|
288
|
+
source: @source,
|
289
|
+
target: target,
|
290
|
+
weapon: using,
|
291
|
+
battle: battle,
|
292
|
+
thrown: thrown,
|
293
|
+
type: :miss,
|
294
|
+
advantage_mod: @advantage_mod,
|
295
|
+
adv_info: adv_info,
|
296
|
+
second_hand: second_hand,
|
297
|
+
damage_roll: damage_roll,
|
298
|
+
attack_roll: attack_roll,
|
299
|
+
as_reaction: !!as_reaction,
|
300
|
+
target_ac: target.armor_class,
|
301
|
+
cover_ac: cover_ac_adjustments,
|
302
|
+
ammo: ammo_type,
|
303
|
+
npc_action: npc_action
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
self
|
308
|
+
end
|
309
|
+
|
310
|
+
# Computes cover armor class adjustment
|
311
|
+
# @param map [Natural20::BattleMap]
|
312
|
+
# @param target [Natural20::Entity]
|
313
|
+
# @return [Integer]
|
314
|
+
def calculate_cover_ac(map, target)
|
315
|
+
cover_calculation(map, @source, target)
|
316
|
+
end
|
317
|
+
|
318
|
+
protected
|
319
|
+
|
320
|
+
# determine eligibility for the 'Protection' fighting style
|
321
|
+
def evaluate_feature_protection(battle, map, target, adv_info)
|
322
|
+
melee_sqaures = target.melee_squares(map, adjacent_only: true)
|
323
|
+
melee_sqaures.each do |pos|
|
324
|
+
entity = map.entity_at(*pos)
|
325
|
+
next if entity == @source
|
326
|
+
next if entity == target
|
327
|
+
next unless entity
|
328
|
+
|
329
|
+
next unless entity.class_feature?('protection') && entity.shield_equipped? && entity.has_reaction?(battle)
|
330
|
+
|
331
|
+
controller = battle.controller_for(entity)
|
332
|
+
if controller.respond_to?(:reaction) && !controller.reaction(:feature_protection, target: target,
|
333
|
+
source: entity, attacker: @source)
|
334
|
+
next
|
335
|
+
end
|
336
|
+
|
337
|
+
Natural20::EventManager.received_event(event: :feature_protection, target: target, source: entity,
|
338
|
+
attacker: @source)
|
339
|
+
_advantage, disadvantage = adv_info
|
340
|
+
disadvantage << :protection
|
341
|
+
@advantage_mod = -1
|
342
|
+
battle.entity_state_for(entity)[:reaction] -= 1
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def check_weapon_bonuses(battle, weapon, damage_roll, attack_roll)
|
347
|
+
if weapon.dig(:bonus, :additional, :restriction) == 'nat20_attack' && attack_roll.nat_20?
|
348
|
+
damage_roll += Natural20::DieRoll.roll(weapon.dig(:bonus, :additional, :die),
|
349
|
+
description: t('dice_roll.special_weapon_damage'), entity: @source, battle: battle)
|
350
|
+
end
|
351
|
+
|
352
|
+
damage_roll
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class TwoWeaponAttackAction < AttackAction
|
357
|
+
# @param entity [Natural20::Entity]
|
358
|
+
# @param battle [Natural20::Battle]
|
359
|
+
def self.can?(entity, battle, options = {})
|
360
|
+
battle.nil? || (entity.total_bonus_actions(battle).positive? && battle.two_weapon_attack?(entity) && options[:weapon] != battle.first_hand_weapon(entity) || entity.equipped_weapons.select do |a|
|
361
|
+
a == battle.first_hand_weapon(entity)
|
362
|
+
end.size >= 2)
|
363
|
+
end
|
364
|
+
|
365
|
+
def second_hand
|
366
|
+
true
|
367
|
+
end
|
368
|
+
|
369
|
+
def label
|
370
|
+
"Bonus Action -> #{super}"
|
371
|
+
end
|
372
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Natural20::ActionDamage
|
2
|
+
def damage_event(item, battle)
|
3
|
+
Natural20::EventManager.received_event({ source: item[:source], attack_roll: item[:attack_roll], target: item[:target], event: :attacked,
|
4
|
+
attack_name: item[:attack_name],
|
5
|
+
damage_type: item[:damage_type],
|
6
|
+
advantage_mod: item[:advantage_mod],
|
7
|
+
as_reaction: item[:as_reaction],
|
8
|
+
damage_roll: item[:damage],
|
9
|
+
sneak_attack: item[:sneak_attack],
|
10
|
+
adv_info: item[:adv_info],
|
11
|
+
value: item[:damage].result + (item[:sneak_attack]&.result.presence || 0) })
|
12
|
+
item[:target].take_damage!(item, battle)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# typed: true
|
2
|
+
class DashAction < Natural20::Action
|
3
|
+
attr_accessor :as_bonus_action
|
4
|
+
|
5
|
+
def build_map
|
6
|
+
OpenStruct.new({
|
7
|
+
param: nil,
|
8
|
+
next: -> { self }
|
9
|
+
})
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.can?(entity, battle)
|
13
|
+
battle && entity.total_actions(battle).positive?
|
14
|
+
end
|
15
|
+
|
16
|
+
def resolve(_session, _map, opts = {})
|
17
|
+
@result = [{
|
18
|
+
source: @source,
|
19
|
+
type: :dash,
|
20
|
+
battle: opts[:battle]
|
21
|
+
}]
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def apply!(battle)
|
26
|
+
@result.each do |item|
|
27
|
+
case (item[:type])
|
28
|
+
when :dash
|
29
|
+
Natural20::EventManager.received_event({ source: item[:source], event: :dash })
|
30
|
+
battle.entity_state_for(item[:source])[:movement] += item[:source].speed
|
31
|
+
end
|
32
|
+
|
33
|
+
if as_bonus_action
|
34
|
+
battle.entity_state_for(item[:source])[:bonus_action] -= 1
|
35
|
+
else
|
36
|
+
battle.entity_state_for(item[:source])[:action] -= 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class DashBonusAction < DashAction
|
43
|
+
def self.can?(entity, battle)
|
44
|
+
battle && entity.class_feature?('cunning_action') && entity.total_bonus_actions(battle) > 0
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: true
|
2
|
+
class DisengageAction < Natural20::Action
|
3
|
+
attr_accessor :as_bonus_action
|
4
|
+
|
5
|
+
def self.can?(entity, battle)
|
6
|
+
battle && battle.combat? && entity.total_actions(battle).positive?
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_map
|
10
|
+
OpenStruct.new({
|
11
|
+
param: nil,
|
12
|
+
next: -> { self }
|
13
|
+
})
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build(session, source)
|
17
|
+
action = DisengageAction.new(session, source, :attack)
|
18
|
+
action.build_map
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve(_session, _map, opts = {})
|
22
|
+
@result = [{
|
23
|
+
source: @source,
|
24
|
+
type: :disengage,
|
25
|
+
battle: opts[:battle]
|
26
|
+
}]
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply!(battle)
|
31
|
+
@result.each do |item|
|
32
|
+
case (item[:type])
|
33
|
+
when :disengage
|
34
|
+
Natural20::EventManager.received_event({ source: item[:source], event: :disengage })
|
35
|
+
item[:source].disengage!(battle)
|
36
|
+
end
|
37
|
+
|
38
|
+
if as_bonus_action
|
39
|
+
battle.entity_state_for(item[:source])[:bonus_action] -= 1
|
40
|
+
else
|
41
|
+
battle.entity_state_for(item[:source])[:action] -= 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class DisengageBonusAction < DisengageAction
|
48
|
+
# @param entity [Natural20::Entity]
|
49
|
+
# @param battle [Natural20::Battle]
|
50
|
+
def self.can?(entity, battle)
|
51
|
+
battle && battle.combat? && entity.any_class_feature?(%w[cunning_action nimble_escape]) && entity.total_bonus_actions(battle) > 0
|
52
|
+
end
|
53
|
+
end
|