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
@@ -22,23 +22,21 @@ class DisengageAction < Natural20::Action
22
22
  @result = [{
23
23
  source: @source,
24
24
  type: :disengage,
25
+ as_bonus_action: as_bonus_action,
25
26
  battle: opts[:battle]
26
27
  }]
27
28
  self
28
29
  end
29
30
 
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
31
+ def self.apply!(battle, item)
32
+ case item[:type]
33
+ when :disengage
34
+ Natural20::EventManager.received_event({ source: item[:source], event: :disengage })
35
+ item[:source].disengage!(battle)
36
+ if item[:as_bonus_action]
37
+ battle.consume(item[:source], :bonus_action)
40
38
  else
41
- battle.entity_state_for(item[:source])[:action] -= 1
39
+ battle.consume(item[:source], :action)
42
40
  end
43
41
  end
44
42
  end
@@ -48,6 +46,7 @@ class DisengageBonusAction < DisengageAction
48
46
  # @param entity [Natural20::Entity]
49
47
  # @param battle [Natural20::Battle]
50
48
  def self.can?(entity, battle)
51
- battle && battle.combat? && entity.any_class_feature?(%w[cunning_action nimble_escape]) && entity.total_bonus_actions(battle) > 0
49
+ battle && battle.combat? && entity.any_class_feature?(%w[cunning_action
50
+ nimble_escape]) && entity.total_bonus_actions(battle) > 0
52
51
  end
53
52
  end
@@ -22,20 +22,19 @@ class DodgeAction < Natural20::Action
22
22
  @result = [{
23
23
  source: @source,
24
24
  type: :dodge,
25
+ as_bonus: as_bonus_action,
25
26
  battle: opts[:battle]
26
27
  }]
27
28
  self
28
29
  end
29
30
 
30
- def apply!(battle)
31
- @result.each do |item|
32
- case (item[:type])
33
- when :dodge
34
- Natural20::EventManager.received_event({ source: item[:source], event: :dodge })
35
- item[:source].dodging!(battle)
36
- end
31
+ def self.apply!(battle, item)
32
+ case (item[:type])
33
+ when :dodge
34
+ Natural20::EventManager.received_event({ source: item[:source], event: :dodge })
35
+ item[:source].dodging!(battle)
37
36
 
38
- if as_bonus_action
37
+ if item[:as_bonus_action]
39
38
  battle.entity_state_for(item[:source])[:bonus_action] -= 1
40
39
  else
41
40
  battle.entity_state_for(item[:source])[:action] -= 1
@@ -73,25 +73,23 @@ class EscapeGrappleAction < Natural20::Action
73
73
  end
74
74
  end
75
75
 
76
- def apply!(battle)
77
- @result.each do |item|
78
- case (item[:type])
79
- when :grapple_escape
80
- if item[:success]
81
- @source.escape_grapple_from!(item[:target])
82
- Natural20::EventManager.received_event(event: :escape_grapple_success,
83
- target: item[:target], source: @source,
84
- source_roll: item[:source_roll],
85
- target_roll: item[:target_roll])
86
- else
87
- Natural20::EventManager.received_event(event: :escape_grapple_failure,
88
- target: item[:target], source: @source,
89
- source_roll: item[:source_roll],
90
- target_roll: item[:target_roll])
91
- end
92
-
93
- battle.entity_state_for(item[:source])[:action] -= 1
76
+ def self.apply!(battle, item)
77
+ case (item[:type])
78
+ when :grapple_escape
79
+ if item[:success]
80
+ item[:source].escape_grapple_from!(item[:target])
81
+ Natural20::EventManager.received_event(event: :escape_grapple_success,
82
+ target: item[:target], source: item[:source],
83
+ source_roll: item[:source_roll],
84
+ target_roll: item[:target_roll])
85
+ else
86
+ Natural20::EventManager.received_event(event: :escape_grapple_failure,
87
+ target: item[:target], source: item[:source],
88
+ source_roll: item[:source_roll],
89
+ target_roll: item[:target_roll])
94
90
  end
91
+
92
+ battle.consume(item[:source], :action)
95
93
  end
96
94
  end
97
95
  end
@@ -88,23 +88,21 @@ class FirstAidAction < Natural20::Action
88
88
  end
89
89
  end
90
90
 
91
- def apply!(battle)
92
- @result.each do |item|
93
- case (item[:type])
94
- when :first_aid
95
- if item[:success]
96
- item[:target].stable!
97
- Natural20::EventManager.received_event(event: :first_aid,
98
- target: item[:target], source: @source,
99
- roll: item[:roll])
100
- else
101
- Natural20::EventManager.received_event(event: :first_aid_failure,
102
- target: item[:target], source: @source,
103
- roll: item[:roll])
104
- end
105
-
106
- battle.entity_state_for(item[:source])[:action] -= 1
91
+ def self.apply!(battle, item)
92
+ case item[:type]
93
+ when :first_aid
94
+ if item[:success]
95
+ item[:target].stable!
96
+ Natural20::EventManager.received_event(event: :first_aid,
97
+ target: item[:target], source: item[:source],
98
+ roll: item[:roll])
99
+ else
100
+ Natural20::EventManager.received_event(event: :first_aid_failure,
101
+ target: item[:target], source: item[:source],
102
+ roll: item[:roll])
107
103
  end
104
+
105
+ battle.consume(item[:source], :action)
108
106
  end
109
107
  end
110
108
  end
@@ -94,25 +94,23 @@ class GrappleAction < Natural20::Action
94
94
  end
95
95
  end
96
96
 
97
- def apply!(battle)
98
- @result.each do |item|
99
- case (item[:type])
100
- when :grapple
101
- if item[:success]
102
- item[:target].grappled_by!(@source)
103
- Natural20::EventManager.received_event(event: :grapple_success,
104
- target: item[:target], source: @source,
105
- source_roll: item[:source_roll],
106
- target_roll: item[:target_roll])
107
- else
108
- Natural20::EventManager.received_event(event: :grapple_failure,
109
- target: item[:target], source: @source,
110
- source_roll: item[:source_roll],
111
- target_roll: item[:target_roll])
112
- end
113
-
114
- battle.entity_state_for(item[:source])[:action] -= 1
97
+ def self.apply!(battle, item)
98
+ case (item[:type])
99
+ when :grapple
100
+ if item[:success]
101
+ item[:target].grappled_by!(item[:source])
102
+ Natural20::EventManager.received_event(event: :grapple_success,
103
+ target: item[:target], source: item[:source],
104
+ source_roll: item[:source_roll],
105
+ target_roll: item[:target_roll])
106
+ else
107
+ Natural20::EventManager.received_event(event: :grapple_failure,
108
+ target: item[:target], source: item[:source],
109
+ source_roll: item[:source_roll],
110
+ target_roll: item[:target_roll])
115
111
  end
112
+
113
+ battle.consume(item[:source], :action)
116
114
  end
117
115
  end
118
116
  end
@@ -169,17 +167,15 @@ class DropGrappleAction < Natural20::Action
169
167
  }]
170
168
  end
171
169
 
172
- def apply!(_battle)
173
- @result.each do |item|
174
- case (item[:type])
175
- when :drop_grapple
176
- item[:target].escape_grapple_from!(@source)
177
- Natural20::EventManager.received_event(event: :drop_grapple,
178
- target: item[:target], source: @source,
179
- source_roll: item[:source_roll],
180
- target_roll: item[:target_roll])
170
+ def self.apply!(_battle, item)
171
+ case item[:type]
172
+ when :drop_grapple
173
+ item[:target].escape_grapple_from!(item[:source])
174
+ Natural20::EventManager.received_event(event: :drop_grapple,
175
+ target: item[:target], source: item[:source],
176
+ source_roll: item[:source_roll],
177
+ target_roll: item[:target_roll])
181
178
 
182
- end
183
179
  end
184
180
  end
185
181
  end
@@ -59,8 +59,7 @@ class GroundInteractAction < Natural20::Action
59
59
  self
60
60
  end
61
61
 
62
- def apply!(battle)
63
- @result.each do |item|
62
+ def self.apply!(battle, item)
64
63
  entity = item[:source]
65
64
  case (item[:type])
66
65
  when :pickup
@@ -74,6 +73,5 @@ class GroundInteractAction < Natural20::Action
74
73
  battle&.consume!(entity, :free_object_interaction, 1) || battle&.consume!(entity, :action, 1)
75
74
  end
76
75
  end
77
- end
78
76
  end
79
77
  end
@@ -14,16 +14,16 @@ class HelpAction < Natural20::Action
14
14
  type: :select_target,
15
15
  target_types: %i[allies enemies],
16
16
  range: 5,
17
- num: 1,
18
- },
17
+ num: 1
18
+ }
19
19
  ],
20
20
  next: lambda { |target|
21
21
  self.target = target
22
22
  OpenStruct.new({
23
- param: nil,
24
- next: -> { self },
25
- })
26
- },
23
+ param: nil,
24
+ next: -> { self }
25
+ })
26
+ }
27
27
  })
28
28
  end
29
29
 
@@ -37,20 +37,17 @@ class HelpAction < Natural20::Action
37
37
  source: @source,
38
38
  target: @target,
39
39
  type: :help,
40
- battle: opts[:battle],
40
+ battle: opts[:battle]
41
41
  }]
42
42
  self
43
43
  end
44
44
 
45
- def apply!(battle)
46
- @result.each do |item|
47
- case (item[:type])
48
- when :help
49
- Natural20::EventManager.received_event({ source: item[:source], target: item[:target], event: :help })
50
- item[:source].help!(item[:battle], item[:target])
51
- end
52
-
53
- battle.entity_state_for(item[:source])[:action] -= 1
45
+ def self.apply!(battle, item)
46
+ case item[:type]
47
+ when :help
48
+ Natural20::EventManager.received_event({ source: item[:source], target: item[:target], event: :help })
49
+ item[:source].help!(item[:battle], item[:target])
50
+ battle.consume!(item[:source], :action)
54
51
  end
55
52
  end
56
53
  end
@@ -30,14 +30,11 @@ class HideAction < Natural20::Action
30
30
  end
31
31
 
32
32
  # @param battle [Natural20::Battle]
33
- def apply!(battle)
34
- @result.each do |item|
35
- case (item[:type])
36
- when :hide
37
- Natural20::EventManager.received_event({ source: item[:source], roll: item[:roll], event: :hide })
38
- item[:source].hiding!(battle, item[:roll].result)
39
- end
40
-
33
+ def self.apply!(battle, item)
34
+ case (item[:type])
35
+ when :hide
36
+ Natural20::EventManager.received_event({ source: item[:source], roll: item[:roll], event: :hide })
37
+ item[:source].hiding!(battle, item[:roll].result)
41
38
  if item[:bonus_action]
42
39
  battle.consume(item[:source], :bonus_action)
43
40
  else
@@ -49,6 +46,7 @@ end
49
46
 
50
47
  class HideBonusAction < HideAction
51
48
  def self.can?(entity, battle)
52
- battle && entity.any_class_feature?(%w[cunning_action nimble_escape]) && entity.total_bonus_actions(battle).positive?
49
+ battle && entity.any_class_feature?(%w[cunning_action
50
+ nimble_escape]) && entity.total_bonus_actions(battle).positive?
53
51
  end
54
52
  end
@@ -71,21 +71,19 @@ class InteractAction < Natural20::Action
71
71
  self
72
72
  end
73
73
 
74
- def apply!(battle)
75
- @result.each do |item|
76
- entity = item[:source]
77
- case (item[:type])
78
- when :interact
79
- item[:target].use!(entity, item)
80
- if item[:cost] == :action
81
- battle&.consume!(entity, :action, 1)
82
- else
83
- battle&.consume!(entity, :free_object_interaction, 1) || battle&.consume!(entity, :action, 1)
84
- end
85
-
86
- Natural20::EventManager.received_event(event: :interact, source: entity, target: item[:target],
87
- object_action: item[:object_action])
74
+ def self.apply!(battle, item)
75
+ entity = item[:source]
76
+ case (item[:type])
77
+ when :interact
78
+ item[:target].use!(entity, item)
79
+ if item[:cost] == :action
80
+ battle&.consume!(entity, :action, 1)
81
+ else
82
+ battle&.consume!(entity, :free_object_interaction, 1) || battle&.consume!(entity, :action, 1)
88
83
  end
84
+
85
+ Natural20::EventManager.received_event(event: :interact, source: entity, target: item[:target],
86
+ object_action: item[:object_action])
89
87
  end
90
88
  end
91
89
  end
@@ -3,7 +3,7 @@ class LookAction < Natural20::Action
3
3
 
4
4
  # @param entity [Natural20::Entity]
5
5
  # @param battle [Natural20::Battle]
6
- def self.can?(entity, battle)
6
+ def self.can?(entity, battle, options = {})
7
7
  battle.nil? || !battle.ongoing? || battle.entity_state_for(entity)[:active_perception].zero?
8
8
  end
9
9
 
@@ -40,24 +40,23 @@ class LookAction < Natural20::Action
40
40
  type: :look,
41
41
  die_roll: perception_check,
42
42
  die_roll_disadvantage: perception_check_disadvantage,
43
- battle: opts[:battle]
43
+ battle: opts[:battle],
44
+ ui_callback: ui_callback
44
45
  }]
45
46
  self
46
47
  end
47
48
 
48
- def apply!(battle)
49
- @result.each do |item|
50
- case (item[:type])
51
- when :look
52
- battle.entity_state_for(item[:source])[:active_perception] = item[:die_roll].result
53
- battle.entity_state_for(item[:source])[:active_perception_disadvantage] = item[:die_roll_disadvantage].result
54
- Natural20::EventManager.received_event({
55
- source: item[:source],
56
- perception_roll: item[:die_roll],
57
- event: :perception
58
- })
59
- ui_callback&.target_ui(item[:source], perception: item[:die_roll].result, look_mode: true)
60
- end
49
+ def self.apply!(battle, item)
50
+ case (item[:type])
51
+ when :look
52
+ battle.entity_state_for(item[:source])[:active_perception] = item[:die_roll].result
53
+ battle.entity_state_for(item[:source])[:active_perception_disadvantage] = item[:die_roll_disadvantage].result
54
+ Natural20::EventManager.received_event({
55
+ source: item[:source],
56
+ perception_roll: item[:die_roll],
57
+ event: :perception
58
+ })
59
+ item[:ui_callback]&.target_ui(item[:source], perception: item[:die_roll].result, look_mode: true)
61
60
  end
62
61
  end
63
62
  end
@@ -1,7 +1,7 @@
1
1
  # typed: true
2
2
  class MoveAction < Natural20::Action
3
3
  include Natural20::MovementHelper
4
- include Natural20::ActionDamage
4
+ extend Natural20::ActionDamage
5
5
 
6
6
  attr_accessor :move_path, :jump_index, :as_dash, :as_bonus_action
7
7
 
@@ -101,6 +101,8 @@ class MoveAction < Natural20::Action
101
101
  battle: battle,
102
102
  type: :move,
103
103
  path: grappled_entity_movement,
104
+ as_dash: as_dash,
105
+ as_bonus_action: as_bonus_action,
104
106
  move_cost: 0,
105
107
  position: grappled_entity_movement.last
106
108
  }
@@ -113,6 +115,8 @@ class MoveAction < Natural20::Action
113
115
  source: @source,
114
116
  map: map,
115
117
  battle: battle,
118
+ as_dash: as_dash,
119
+ as_bonus_action: as_bonus_action,
116
120
  type: :move,
117
121
  path: movement.movement,
118
122
  move_cost: movement_budget - movement.budget,
@@ -140,15 +144,12 @@ class MoveAction < Natural20::Action
140
144
  move_list
141
145
  end
142
146
 
143
- def apply!(battle)
144
- @result.each do |item|
147
+ def self.apply!(battle, item)
145
148
  case (item[:type])
146
149
  when :state
147
150
  item[:params].each do |k, v|
148
151
  item[:source].send(:"#{k}=", v)
149
152
  end
150
- when :damage
151
- damage_event(item, battle)
152
153
  when :acrobatics, :athletics
153
154
  if item[:success]
154
155
  Natural20::EventManager.received_event(source: item[:source], event: item[:type], success: true,
@@ -167,9 +168,9 @@ class MoveAction < Natural20::Action
167
168
 
168
169
  when :move
169
170
  item[:map].move_to!(item[:source], *item[:position], battle)
170
- if as_dash && as_bonus_action
171
+ if item[:as_dash] && item[:as_bonus_action]
171
172
  battle.entity_state_for(item[:source])[:bonus_action] -= 1
172
- elsif as_dash
173
+ elsif item[:as_dash]
173
174
  battle.entity_state_for(item[:source])[:action] -= 1
174
175
  elsif battle
175
176
  battle.entity_state_for(item[:source])[:movement] -= item[:move_cost] * battle.map.feet_per_grid
@@ -177,9 +178,8 @@ class MoveAction < Natural20::Action
177
178
 
178
179
  Natural20::EventManager.received_event({ event: :move, source: item[:source], position: item[:position], path: item[:path],
179
180
  feet_per_grid: battle.map&.feet_per_grid,
180
- as_dash: as_dash, as_bonus: as_bonus_action })
181
+ as_dash: item[:as_dash], as_bonus: item[:as_bonus_action] })
181
182
  end
182
- end
183
183
  end
184
184
 
185
185
  private