natural_20 0.1.1 → 0.2.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +13 -0
  4. data/README.md +1 -0
  5. data/bin/nat20 +2 -1
  6. data/bin/nat20.cmd +0 -0
  7. data/char_classes/wizard.yml +89 -0
  8. data/characters/high_elf_mage.yml +27 -0
  9. data/fixtures/battle_sim_objects.yml +2 -2
  10. data/fixtures/high_elf_mage.yml +28 -0
  11. data/fixtures/large_map.yml +63 -0
  12. data/game.yml +2 -2
  13. data/items/equipment.yml +30 -0
  14. data/items/objects.yml +33 -29
  15. data/items/spells.yml +58 -0
  16. data/items/weapons.yml +78 -18
  17. data/lib/CHANGELOG.md +0 -0
  18. data/lib/natural_20.rb +9 -0
  19. data/lib/natural_20/actions/action.rb +2 -2
  20. data/lib/natural_20/actions/attack_action.rb +76 -67
  21. data/lib/natural_20/actions/concerns/action_damage.rb +3 -1
  22. data/lib/natural_20/actions/dash_action.rb +7 -10
  23. data/lib/natural_20/actions/disengage_action.rb +11 -12
  24. data/lib/natural_20/actions/dodge_action.rb +7 -8
  25. data/lib/natural_20/actions/escape_grapple_action.rb +16 -18
  26. data/lib/natural_20/actions/first_aid_action.rb +14 -16
  27. data/lib/natural_20/actions/grapple_action.rb +24 -28
  28. data/lib/natural_20/actions/ground_interact_action.rb +1 -3
  29. data/lib/natural_20/actions/help_action.rb +13 -16
  30. data/lib/natural_20/actions/hide_action.rb +7 -9
  31. data/lib/natural_20/actions/interact_action.rb +12 -14
  32. data/lib/natural_20/actions/look_action.rb +14 -15
  33. data/lib/natural_20/actions/move_action.rb +9 -9
  34. data/lib/natural_20/actions/multiattack_action.rb +8 -9
  35. data/lib/natural_20/actions/prone_action.rb +4 -6
  36. data/lib/natural_20/actions/short_rest_action.rb +7 -8
  37. data/lib/natural_20/actions/shove_action.rb +20 -24
  38. data/lib/natural_20/actions/spell_action.rb +89 -0
  39. data/lib/natural_20/actions/stand_action.rb +5 -7
  40. data/lib/natural_20/actions/use_item_action.rb +7 -9
  41. data/lib/natural_20/ai_controller/standard.rb +1 -1
  42. data/lib/natural_20/battle.rb +8 -3
  43. data/lib/natural_20/cli/action_ui.rb +180 -0
  44. data/lib/natural_20/cli/builder/fighter_builder.rb +1 -1
  45. data/lib/natural_20/cli/builder/rogue_builder.rb +10 -10
  46. data/lib/natural_20/cli/builder/wizard_builder.rb +77 -0
  47. data/lib/natural_20/cli/character_builder.rb +9 -4
  48. data/lib/natural_20/cli/commandline_ui.rb +55 -162
  49. data/lib/natural_20/cli/inventory_ui.rb +4 -0
  50. data/lib/natural_20/cli/map_renderer.rb +7 -1
  51. data/lib/natural_20/concerns/attack_helper.rb +53 -0
  52. data/lib/natural_20/concerns/entity.rb +170 -11
  53. data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +7 -9
  54. data/lib/natural_20/concerns/fighter_class.rb +2 -2
  55. data/lib/natural_20/concerns/spell_attack_helper.rb +33 -0
  56. data/lib/natural_20/concerns/wizard_class.rb +86 -0
  57. data/lib/natural_20/die_roll.rb +2 -2
  58. data/lib/natural_20/event_manager.rb +50 -44
  59. data/lib/natural_20/item_library/base_item.rb +1 -1
  60. data/lib/natural_20/npc.rb +4 -0
  61. data/lib/natural_20/player_character.rb +75 -12
  62. data/lib/natural_20/session.rb +14 -1
  63. data/lib/natural_20/spell_library/firebolt.rb +72 -0
  64. data/lib/natural_20/spell_library/mage_armor.rb +67 -0
  65. data/lib/natural_20/spell_library/mage_hand.rb +2 -0
  66. data/lib/natural_20/spell_library/magic_missile.rb +67 -0
  67. data/lib/natural_20/spell_library/shield.rb +69 -0
  68. data/lib/natural_20/spell_library/spell.rb +31 -0
  69. data/lib/natural_20/utils/weapons.rb +8 -6
  70. data/lib/natural_20/version.rb +1 -1
  71. data/locales/en.yml +44 -8
  72. data/maps/game_map.yml +12 -2
  73. metadata +22 -3
data/items/spells.yml ADDED
@@ -0,0 +1,58 @@
1
+ ---
2
+ firebolt:
3
+ base_damage: 1d10
4
+ casting_time: 1:action
5
+ components:
6
+ - verbal
7
+ - sematic
8
+ damage_increase:
9
+ - 5
10
+ - 11
11
+ - 17
12
+ damage_type: fire
13
+ description: You hurl a mote of fire at a creature or object within range. A flammable object hit by this spell ignites if it isn't being worn or carried.
14
+ duration: instant
15
+ level: 0
16
+ name: Firebolt
17
+ range: 120
18
+ school: evocation
19
+ spell_class: Natural20::Firebolt
20
+ type: ranged_attack
21
+ mage_armor:
22
+ casting_time: 1:action
23
+ components:
24
+ - verbal
25
+ - sematic
26
+ - material
27
+ description: You touch a willing creature who isn't wearing armor, and a protective magical force surrounds it until the spell ends. The target's base AC becomes 13 + its Dexterity modifier. The spell ends if the target dons armor or if you dismiss the spell as an action.
28
+ duration: 8h
29
+ level: 1
30
+ materials:
31
+ - cured_leather
32
+ name: Mage Armor
33
+ school: abjuration
34
+ spell_class: Natural20::MageArmor
35
+ type: buff
36
+ magic_missile:
37
+ casting_time: 1:action
38
+ damage_type: force
39
+ level: 1
40
+ name: Magic Missile
41
+ range: 120
42
+ school: evocation
43
+ description: You create three glowing darts of magical force. Each dart hits a creature of your choice that you can see within range.
44
+ spell_class: Natural20::MagicMissile
45
+ type: force
46
+ higher_level: true
47
+ shield:
48
+ casting_time: 1:reaction
49
+ level: 1
50
+ name: Shield
51
+ range: 0
52
+ duration: 1R
53
+ school: abjuration
54
+ spell_class: Natural20::Shield
55
+ components:
56
+ - verbal
57
+ - sematic
58
+ type: warding
data/items/weapons.yml CHANGED
@@ -39,6 +39,66 @@ dagger:
39
39
  range_max: 120
40
40
  type: melee_attack
41
41
  weight: 10
42
+ quarterstaff:
43
+ cost: 2sp
44
+ damage: 1d6
45
+ damage_2: 1d8
46
+ damage_type: bludgeoning
47
+ meta:
48
+ noise_source: 5
49
+ noise_target: 5
50
+ modifiers: null
51
+ name: Quarterstaff
52
+ proficiency_type:
53
+ - simple
54
+ properties:
55
+ - versatile
56
+ range: 5
57
+ subtype: weapon
58
+ type: melee_attack
59
+ weight: 4
60
+ sling:
61
+ ammo: pellet
62
+ cost: 1sp
63
+ damage: 1d4
64
+ damage_type: bludgeoning
65
+ meta:
66
+ noise_source: 5
67
+ noise_target: 5
68
+ modifiers: null
69
+ name: Sling
70
+ proficiency_type:
71
+ - simple
72
+ properties:
73
+ - ammunition
74
+ - ranged
75
+ range: 30
76
+ range_max: 120
77
+ subtype: weapon
78
+ type: ranged_attack
79
+ weight: 0
80
+ dart:
81
+ ammo: dart
82
+ cost: 2
83
+ damage: 1d4
84
+ damage_type: piercing
85
+ meta:
86
+ noise_source: 5
87
+ noise_target: 5
88
+ modifiers: null
89
+ name: Dart
90
+ proficiency_type:
91
+ - simple
92
+ properties:
93
+ - thrown
94
+ - finesse
95
+ range: 5
96
+ subtype: weapon
97
+ thrown:
98
+ range: 20
99
+ range_max: 60
100
+ type: ranged_attack
101
+ weight: 0.25
42
102
  greatclub:
43
103
  cost: 2sp
44
104
  damage: 1d8
@@ -162,24 +222,6 @@ light_hammer:
162
222
  range_max: 60
163
223
  type: melee_attack
164
224
  weight: 2
165
- warhammer:
166
- cost: 2
167
- damage: 1d8
168
- damage_2: 1d10
169
- damage_type: bludgeoning
170
- meta:
171
- noise_source: 5
172
- noise_target: 5
173
- modifiers: null
174
- name: Warhammer
175
- proficiency_type:
176
- - martial
177
- properties:
178
- - versatile
179
- range: 5
180
- subtype: weapon
181
- type: melee_attack
182
- weight: 2
183
225
  longbow:
184
226
  ammo: arrows
185
227
  cost: 50
@@ -351,4 +393,22 @@ vicious_rapier:
351
393
  rarity: very_rare
352
394
  subtype: weapon
353
395
  type: melee_attack
396
+ weight: 2
397
+ warhammer:
398
+ cost: 2
399
+ damage: 1d8
400
+ damage_2: 1d10
401
+ damage_type: bludgeoning
402
+ meta:
403
+ noise_source: 5
404
+ noise_target: 5
405
+ modifiers: null
406
+ name: Warhammer
407
+ proficiency_type:
408
+ - martial
409
+ properties:
410
+ - versatile
411
+ range: 5
412
+ subtype: weapon
413
+ type: melee_attack
354
414
  weight: 2
data/lib/CHANGELOG.md ADDED
File without changes
data/lib/natural_20.rb CHANGED
@@ -9,6 +9,7 @@ require 'natural_20/concerns/lootable'
9
9
  require 'natural_20/concerns/evaluator/entity_state_evaluator'
10
10
  require 'natural_20/concerns/entity'
11
11
  require 'natural_20/item_library/object'
12
+ require 'natural_20/concerns/attack_helper'
12
13
  require 'natural_20/concerns/movement_helper'
13
14
  require 'natural_20/utils/cover'
14
15
  require 'natural_20/utils/weapons'
@@ -16,6 +17,7 @@ require 'natural_20/concerns/navigation'
16
17
  require 'natural_20/actions/action'
17
18
  require 'natural_20/concerns/fighter_class'
18
19
  require 'natural_20/concerns/rogue_class'
20
+ require 'natural_20/concerns/wizard_class'
19
21
  require 'natural_20/actions/concerns/action_damage'
20
22
  require 'natural_20/actions/look_action'
21
23
  require 'natural_20/actions/attack_action'
@@ -37,6 +39,13 @@ require 'natural_20/actions/escape_grapple_action'
37
39
  require 'natural_20/actions/ground_interact_action'
38
40
  require 'natural_20/actions/first_aid_action'
39
41
  require 'natural_20/actions/shove_action'
42
+ require 'natural_20/concerns/spell_attack_helper'
43
+ require 'natural_20/spell_library/spell'
44
+ require 'natural_20/spell_library/mage_armor'
45
+ require 'natural_20/spell_library/firebolt'
46
+ require 'natural_20/spell_library/magic_missile'
47
+ require 'natural_20/spell_library/shield'
48
+ require 'natural_20/actions/spell_action'
40
49
  require 'natural_20/battle'
41
50
  require 'natural_20/utils/ray_tracer'
42
51
  require 'natural_20/battle_map'
@@ -27,14 +27,14 @@ module Natural20
27
27
  def validate
28
28
  end
29
29
 
30
- def apply!(battle); end
30
+ def self.apply!(battle, item); end
31
31
 
32
32
  def resolve(session, map, opts = {}); end
33
33
 
34
34
  protected
35
35
 
36
36
  def t(k, options = {})
37
- I18n.t(k, options)
37
+ I18n.t(k, **options)
38
38
  end
39
39
  end
40
40
  end
@@ -2,7 +2,8 @@
2
2
  class AttackAction < Natural20::Action
3
3
  include Natural20::Cover
4
4
  include Natural20::Weapons
5
- include Natural20::ActionDamage
5
+ include Natural20::AttackHelper
6
+ extend Natural20::ActionDamage
6
7
 
7
8
  attr_accessor :target, :using, :npc_action, :as_reaction, :thrown, :second_hand
8
9
  attr_reader :advantage_mod
@@ -31,7 +32,7 @@ class AttackAction < Natural20::Action
31
32
 
32
33
  i18n_token = thrown ? 'action.attack_action_throw' : 'action.attack_action'
33
34
 
34
- t(i18n_token, name: @action_type.to_s.humanize, weapon_name: weapon[:name], mod: attack_mod,
35
+ t(i18n_token, name: @action_type.to_s.humanize, weapon_name: weapon[:name], mod: attack_mod >= 0 ? "+#{attack_mod}" : attack_mod,
35
36
  dmg: damage_modifier(@source, weapon, second_hand: second_hand))
36
37
  end
37
38
  end
@@ -70,76 +71,82 @@ class AttackAction < Natural20::Action
70
71
  end
71
72
 
72
73
  # @param battle [Natural20::Battle]
73
- def apply!(battle)
74
- @result.each do |item|
75
- if item[:flavor]
76
- Natural20::EventManager.received_event({ event: :flavor, source: item[:source], target: item[:target],
77
- text: item[:flavor] })
78
- end
79
- case (item[:type])
80
- when :prone
81
- item[:source].prone!
82
- when :damage
83
- damage_event(item, battle)
84
- when :miss
85
- Natural20::EventManager.received_event({ attack_roll: item[:attack_roll],
86
- attack_name: item[:attack_name],
87
- advantage_mod: item[:advantage_mod],
88
- as_reaction: !!as_reaction,
89
- adv_info: item[:adv_info],
90
- source: item[:source], target: item[:target], event: :miss })
91
- end
92
-
93
- # handle ammo
94
- item[:source].deduct_item(item[:ammo], 1) if item[:ammo]
95
-
96
- # hanle thrown items
97
- if item[:thrown]
98
- if item[:source].item_count(item[:weapon]).positive?
99
- item[:source].deduct_item(item[:weapon], 1)
100
- else
101
- item[:source].unequip(item[:weapon], transfer_inventory: false)
102
- end
74
+ def self.apply!(battle, item)
75
+ if item[:flavor]
76
+ Natural20::EventManager.received_event({ event: :flavor, source: item[:source], target: item[:target],
77
+ text: item[:flavor] })
78
+ end
79
+ case (item[:type])
80
+ when :prone
81
+ item[:source].prone!
82
+ when :damage
83
+ damage_event(item, battle)
84
+ consume_resource(battle, item)
85
+ when :miss
86
+ consume_resource(battle, item)
87
+ Natural20::EventManager.received_event({ attack_roll: item[:attack_roll],
88
+ attack_name: item[:attack_name],
89
+ advantage_mod: item[:advantage_mod],
90
+ as_reaction: !!item[:as_reaction],
91
+ adv_info: item[:adv_info],
92
+ source: item[:source], target: item[:target], event: :miss })
93
+ end
94
+ end
103
95
 
104
- if item[:type] == :damage
105
- item[:target].add_item(item[:weapon])
106
- else
107
- ground_pos = item[:battle].map.entity_or_object_pos(item[:target])
108
- ground_object = item[:battle].map.objects_at(*ground_pos).detect { |o| o.is_a?(ItemLibrary::Ground) }
109
- ground_object&.add_item(item[:weapon])
110
- end
96
+ # @param battle [Natural20::Battle]
97
+ # @param item [Hash]
98
+ def self.consume_resource(battle, item)
99
+ # handle ammo
100
+ item[:source].deduct_item(item[:ammo], 1) if item[:ammo]
101
+
102
+ # hanle thrown items
103
+ if item[:thrown]
104
+ if item[:source].item_count(item[:weapon]).positive?
105
+ item[:source].deduct_item(item[:weapon], 1)
106
+ else
107
+ item[:source].unequip(item[:weapon], transfer_inventory: false)
111
108
  end
112
109
 
113
- if as_reaction
114
- battle.consume(item[:source], :reaction)
115
- elsif item[:second_hand]
116
- battle.consume(item[:source], :bonus_action)
110
+ if item[:type] == :damage
111
+ item[:target].add_item(item[:weapon])
117
112
  else
118
- battle.consume(item[:source], :action)
113
+ ground_pos = item[:battle].map.entity_or_object_pos(item[:target])
114
+ ground_object = item[:battle].map.objects_at(*ground_pos).detect { |o| o.is_a?(ItemLibrary::Ground) }
115
+ ground_object&.add_item(item[:weapon])
119
116
  end
117
+ end
120
118
 
121
- item[:source].break_stealth!(battle)
119
+ if item[:as_reaction]
120
+ battle.consume(item[:source], :reaction)
121
+ elsif item[:second_hand]
122
+ battle.consume(item[:source], :bonus_action)
123
+ else
124
+ battle.consume(item[:source], :action)
125
+ end
122
126
 
123
- # handle two-weapon fighting
124
- weapon = session.load_weapon(item[:weapon]) if item[:weapon]
127
+ item[:source].break_stealth!(battle)
125
128
 
126
- if weapon && weapon[:properties]&.include?('light') && !battle.two_weapon_attack?(item[:source]) && !item[:second_hand]
127
- battle.entity_state_for(item[:source])[:two_weapon] = item[:weapon]
128
- else
129
- battle.entity_state_for(item[:source])[:two_weapon] = nil
130
- end
129
+ # handle two-weapon fighting
130
+ weapon = battle.session.load_weapon(item[:weapon]) if item[:weapon]
131
131
 
132
- # handle multiattacks
132
+ if weapon && weapon[:properties]&.include?('light') && !battle.two_weapon_attack?(item[:source]) && !item[:second_hand]
133
+ battle.entity_state_for(item[:source])[:two_weapon] = item[:weapon]
134
+ elsif battle.entity_state_for(item[:source])
135
+ battle.entity_state_for(item[:source])[:two_weapon] = nil
136
+ end
137
+
138
+ # handle multiattacks
139
+ if battle.entity_state_for(item[:source])
133
140
  battle.entity_state_for(item[:source])[:multiattack]&.each do |_group, attacks|
134
141
  if attacks.include?(item[:attack_name])
135
142
  attacks.delete(item[:attack_name])
136
143
  item[:source].clear_multiattack!(battle) if attacks.empty?
137
144
  end
138
145
  end
139
-
140
- # dismiss help actions
141
- battle.dismiss_help_for(item[:target])
142
146
  end
147
+
148
+ # dismiss help actions
149
+ battle.dismiss_help_for(item[:target])
143
150
  end
144
151
 
145
152
  def with_advantage?
@@ -172,7 +179,13 @@ class AttackAction < Natural20::Action
172
179
 
173
180
  npc_action = @source.npc_actions.detect { |a| a[:name].downcase == using.downcase } if @source.npc? && using
174
181
 
175
- if npc_action
182
+ if @source.npc?
183
+
184
+ if npc_action.nil?
185
+ npc_action = @source.properties[actions].detect do |action|
186
+ action[:name].downcase == using.to_s.downcase
187
+ end
188
+ end
176
189
  weapon = npc_action
177
190
  attack_name = npc_action[:name]
178
191
  attack_mod = npc_action[:attack]
@@ -199,6 +212,8 @@ class AttackAction < Natural20::Action
199
212
 
200
213
  # handle the lucky feat
201
214
  attack_roll = attack_roll.reroll(lucky: true) if @source.class_feature?('lucky') && attack_roll.nat_1?
215
+ target_ac, _cover_ac = effective_ac(battle, target)
216
+ after_attack_roll_hook(battle, target, source, attack_roll, target_ac)
202
217
 
203
218
  if @source.class_feature?('sneak_attack') && (weapon[:properties]&.include?('finesse') || weapon[:type] == 'ranged_attack') && (with_advantage? || battle.enemy_in_melee_range?(
204
219
  target, [@source]
@@ -233,8 +248,8 @@ class AttackAction < Natural20::Action
233
248
  elsif attack_roll.nat_1?
234
249
  false
235
250
  else
236
- cover_ac_adjustments = calculate_cover_ac(battle.map, target) if battle.map
237
- attack_roll.result >= (target.armor_class + cover_ac_adjustments)
251
+ target_ac, cover_ac_adjustments = effective_ac(battle, target)
252
+ attack_roll.result >= target_ac
238
253
  end
239
254
 
240
255
  if hit
@@ -309,14 +324,6 @@ class AttackAction < Natural20::Action
309
324
  self
310
325
  end
311
326
 
312
- # Computes cover armor class adjustment
313
- # @param map [Natural20::BattleMap]
314
- # @param target [Natural20::Entity]
315
- # @return [Integer]
316
- def calculate_cover_ac(map, target)
317
- cover_calculation(map, @source, target)
318
- end
319
-
320
327
  protected
321
328
 
322
329
  # determine eligibility for the 'Protection' fighting style
@@ -371,4 +378,6 @@ class TwoWeaponAttackAction < AttackAction
371
378
  def label
372
379
  "Bonus Action -> #{super}"
373
380
  end
381
+
382
+ def self.apply!(battle, item); end
374
383
  end
@@ -1,6 +1,8 @@
1
1
  module Natural20::ActionDamage
2
2
  def damage_event(item, battle)
3
- Natural20::EventManager.received_event({ source: item[:source], attack_roll: item[:attack_roll], target: item[:target], event: :attacked,
3
+ Natural20::EventManager.received_event({ source: item[:source],
4
+ attack_roll: item[:attack_roll],
5
+ target: item[:target], event: :attacked,
4
6
  attack_name: item[:attack_name],
5
7
  damage_type: item[:damage_type],
6
8
  advantage_mod: item[:advantage_mod],
@@ -9,7 +9,7 @@ class DashAction < Natural20::Action
9
9
  })
10
10
  end
11
11
 
12
- def self.can?(entity, battle)
12
+ def self.can?(entity, battle, _options = {})
13
13
  battle && entity.total_actions(battle).positive?
14
14
  end
15
15
 
@@ -22,14 +22,11 @@ class DashAction < Natural20::Action
22
22
  self
23
23
  end
24
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
-
25
+ def self.apply!(battle, item)
26
+ case item[:type]
27
+ when :dash
28
+ Natural20::EventManager.received_event({ source: item[:source], event: :dash })
29
+ battle.entity_state_for(item[:source])[:movement] += item[:source].speed
33
30
  if as_bonus_action
34
31
  battle.entity_state_for(item[:source])[:bonus_action] -= 1
35
32
  else
@@ -40,7 +37,7 @@ class DashAction < Natural20::Action
40
37
  end
41
38
 
42
39
  class DashBonusAction < DashAction
43
- def self.can?(entity, battle)
40
+ def self.can?(entity, battle, options = {})
44
41
  battle && entity.class_feature?('cunning_action') && entity.total_bonus_actions(battle) > 0
45
42
  end
46
43
  end