natural_20 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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