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
@@ -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