natural_20 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +99 -0
  9. data/Rakefile +6 -0
  10. data/bin/compute_lights +19 -0
  11. data/bin/console +19 -0
  12. data/bin/nat20 +135 -0
  13. data/bin/nat20.cmd +3 -0
  14. data/bin/nat20author +104 -0
  15. data/bin/setup +8 -0
  16. data/char_classes/fighter.yml +45 -0
  17. data/char_classes/rogue.yml +54 -0
  18. data/characters/halfling_rogue.yml +46 -0
  19. data/characters/high_elf_fighter.yml +49 -0
  20. data/fixtures/battle_sim.yml +58 -0
  21. data/fixtures/battle_sim_2.yml +30 -0
  22. data/fixtures/battle_sim_3.yml +26 -0
  23. data/fixtures/battle_sim_4.yml +26 -0
  24. data/fixtures/battle_sim_objects.yml +101 -0
  25. data/fixtures/corridors.yml +24 -0
  26. data/fixtures/elf_rogue.yml +39 -0
  27. data/fixtures/halfling_rogue.yml +41 -0
  28. data/fixtures/high_elf_fighter.yml +49 -0
  29. data/fixtures/human_fighter.yml +48 -0
  30. data/fixtures/path_finding_test.yml +11 -0
  31. data/fixtures/path_finding_test_2.yml +15 -0
  32. data/fixtures/path_finding_test_3.yml +26 -0
  33. data/fixtures/thin_walls.yml +53 -0
  34. data/fixtures/traps.yml +25 -0
  35. data/game.yml +20 -0
  36. data/items/equipment.yml +101 -0
  37. data/items/objects.yml +73 -0
  38. data/items/weapons.yml +297 -0
  39. data/lib/natural_20.rb +68 -0
  40. data/lib/natural_20/actions/action.rb +40 -0
  41. data/lib/natural_20/actions/attack_action.rb +372 -0
  42. data/lib/natural_20/actions/concerns/action_damage.rb +14 -0
  43. data/lib/natural_20/actions/dash_action.rb +46 -0
  44. data/lib/natural_20/actions/disengage_action.rb +53 -0
  45. data/lib/natural_20/actions/dodge_action.rb +45 -0
  46. data/lib/natural_20/actions/escape_grapple_action.rb +97 -0
  47. data/lib/natural_20/actions/first_aid_action.rb +109 -0
  48. data/lib/natural_20/actions/grapple_action.rb +185 -0
  49. data/lib/natural_20/actions/ground_interact_action.rb +74 -0
  50. data/lib/natural_20/actions/help_action.rb +56 -0
  51. data/lib/natural_20/actions/hide_action.rb +53 -0
  52. data/lib/natural_20/actions/interact_action.rb +91 -0
  53. data/lib/natural_20/actions/inventory_action.rb +23 -0
  54. data/lib/natural_20/actions/look_action.rb +63 -0
  55. data/lib/natural_20/actions/move_action.rb +254 -0
  56. data/lib/natural_20/actions/multiattack_action.rb +41 -0
  57. data/lib/natural_20/actions/prone_action.rb +38 -0
  58. data/lib/natural_20/actions/short_rest_action.rb +53 -0
  59. data/lib/natural_20/actions/shove_action.rb +142 -0
  60. data/lib/natural_20/actions/stand_action.rb +47 -0
  61. data/lib/natural_20/actions/use_item_action.rb +57 -0
  62. data/lib/natural_20/ai_controller/path_compute.rb +140 -0
  63. data/lib/natural_20/ai_controller/standard.rb +288 -0
  64. data/lib/natural_20/battle.rb +544 -0
  65. data/lib/natural_20/battle_map.rb +843 -0
  66. data/lib/natural_20/cli/builder/fighter_builder.rb +104 -0
  67. data/lib/natural_20/cli/builder/rogue_builder.rb +62 -0
  68. data/lib/natural_20/cli/character_builder.rb +210 -0
  69. data/lib/natural_20/cli/commandline_ui.rb +612 -0
  70. data/lib/natural_20/cli/inventory_ui.rb +136 -0
  71. data/lib/natural_20/cli/map_renderer.rb +165 -0
  72. data/lib/natural_20/concerns/container.rb +32 -0
  73. data/lib/natural_20/concerns/entity.rb +1213 -0
  74. data/lib/natural_20/concerns/evaluator/entity_state_evaluator.rb +59 -0
  75. data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +51 -0
  76. data/lib/natural_20/concerns/fighter_class.rb +35 -0
  77. data/lib/natural_20/concerns/health_flavor.rb +27 -0
  78. data/lib/natural_20/concerns/lootable.rb +94 -0
  79. data/lib/natural_20/concerns/movement_helper.rb +195 -0
  80. data/lib/natural_20/concerns/multiattack.rb +54 -0
  81. data/lib/natural_20/concerns/navigation.rb +87 -0
  82. data/lib/natural_20/concerns/notable.rb +37 -0
  83. data/lib/natural_20/concerns/rogue_class.rb +26 -0
  84. data/lib/natural_20/controller.rb +11 -0
  85. data/lib/natural_20/die_roll.rb +331 -0
  86. data/lib/natural_20/event_manager.rb +288 -0
  87. data/lib/natural_20/item_library/base_item.rb +27 -0
  88. data/lib/natural_20/item_library/chest.rb +230 -0
  89. data/lib/natural_20/item_library/door_object.rb +189 -0
  90. data/lib/natural_20/item_library/ground.rb +124 -0
  91. data/lib/natural_20/item_library/healing_potion.rb +51 -0
  92. data/lib/natural_20/item_library/object.rb +153 -0
  93. data/lib/natural_20/item_library/pit_trap.rb +69 -0
  94. data/lib/natural_20/item_library/stone_wall.rb +18 -0
  95. data/lib/natural_20/npc.rb +173 -0
  96. data/lib/natural_20/player_character.rb +414 -0
  97. data/lib/natural_20/session.rb +168 -0
  98. data/lib/natural_20/utils/cover.rb +35 -0
  99. data/lib/natural_20/utils/ray_tracer.rb +90 -0
  100. data/lib/natural_20/utils/static_light_builder.rb +72 -0
  101. data/lib/natural_20/utils/weapons.rb +78 -0
  102. data/lib/natural_20/version.rb +4 -0
  103. data/locales/en.yml +304 -0
  104. data/maps/game_map.yml +168 -0
  105. data/natural_20.gemspec +46 -0
  106. data/npcs/goblin.yml +64 -0
  107. data/npcs/human_guard.yml +48 -0
  108. data/npcs/ogre.yml +61 -0
  109. data/npcs/owlbear.yml +55 -0
  110. data/npcs/wolf.yml +46 -0
  111. data/races/elf.yml +44 -0
  112. data/races/halfling.yml +22 -0
  113. data/races/human.yml +13 -0
  114. metadata +373 -0
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ class HelpAction < Natural20::Action
3
+ attr_accessor :target
4
+
5
+ def self.can?(entity, battle)
6
+ battle && entity.total_actions(battle).positive?
7
+ end
8
+
9
+ def build_map
10
+ OpenStruct.new({
11
+ action: self,
12
+ param: [
13
+ {
14
+ type: :select_target,
15
+ target_types: %i[allies enemies],
16
+ range: 5,
17
+ num: 1,
18
+ },
19
+ ],
20
+ next: lambda { |target|
21
+ self.target = target
22
+ OpenStruct.new({
23
+ param: nil,
24
+ next: -> { self },
25
+ })
26
+ },
27
+ })
28
+ end
29
+
30
+ def self.build(session, source)
31
+ action = HelpAction.new(session, source, :help)
32
+ action.build_map
33
+ end
34
+
35
+ def resolve(_session, _map, opts = {})
36
+ @result = [{
37
+ source: @source,
38
+ target: @target,
39
+ type: :help,
40
+ battle: opts[:battle],
41
+ }]
42
+ self
43
+ end
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
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ class HideAction < Natural20::Action
2
+ attr_accessor :as_bonus_action
3
+
4
+ def self.can?(entity, battle)
5
+ battle && entity.total_actions(battle).positive?
6
+ end
7
+
8
+ def build_map
9
+ OpenStruct.new({
10
+ param: nil,
11
+ next: -> { self }
12
+ })
13
+ end
14
+
15
+ def self.build(session, source)
16
+ action = HideAction.new(session, source, :attack)
17
+ action.build_map
18
+ end
19
+
20
+ def resolve(_session, _map, opts = {})
21
+ stealth_roll = @source.stealth_check!(opts[:battle])
22
+ @result = [{
23
+ source: @source,
24
+ type: :hide,
25
+ roll: stealth_roll,
26
+ battle: opts[:battle]
27
+ }]
28
+ self
29
+ end
30
+
31
+ # @param battle [Natural20::Battle]
32
+ def apply!(battle)
33
+ @result.each do |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)
38
+ end
39
+
40
+ if as_bonus_action
41
+ battle.entity_state_for(item[:source])[:bonus_action] -= 1
42
+ else
43
+ battle.entity_state_for(item[:source])[:action] -= 1
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ class HideBonusAction < HideAction
50
+ def self.can?(entity, battle)
51
+ battle && entity.any_class_feature?(%w[cunning_action nimble_escape]) && entity.total_bonus_actions(battle).positive?
52
+ end
53
+ end
@@ -0,0 +1,91 @@
1
+ # typed: true
2
+ class InteractAction < Natural20::Action
3
+ attr_accessor :target, :object_action, :other_params
4
+
5
+ # @param entity [Natural20::Entity]
6
+ # @param battle [Natural20::Battle]
7
+ def self.can?(entity, battle)
8
+ battle.nil? || !battle.ongoing? || entity.total_actions(battle).positive? || entity.free_object_interaction?(battle)
9
+ end
10
+
11
+ def self.build(session, source)
12
+ action = InteractAction.new(session, source, :attack)
13
+ action.build_map
14
+ end
15
+
16
+ def build_map
17
+ OpenStruct.new({
18
+ action: self,
19
+ param: [
20
+ {
21
+ type: :select_object
22
+ }
23
+ ],
24
+ next: lambda { |object|
25
+ self.target = object
26
+ OpenStruct.new({
27
+ param: [
28
+ {
29
+ type: :interact,
30
+ target: object
31
+ }
32
+ ],
33
+ next: lambda { |action|
34
+ self.object_action = action
35
+
36
+ custom_action = object.try(:build_map, action, self)
37
+
38
+ if custom_action.nil?
39
+ OpenStruct.new({
40
+ param: nil,
41
+ next: lambda {
42
+ self
43
+ }
44
+ })
45
+ else
46
+ custom_action
47
+ end
48
+ }
49
+
50
+ })
51
+ }
52
+ })
53
+ end
54
+
55
+ def resolve(_session, map = nil, opts = {})
56
+ battle = opts[:battle]
57
+
58
+ result = target.resolve(@source, object_action, other_params, opts)
59
+
60
+ return [] if result.nil?
61
+
62
+ result_payload = {
63
+ source: @source,
64
+ target: target,
65
+ object_action: object_action,
66
+ map: map,
67
+ battle: battle,
68
+ type: :interact
69
+ }.merge(result)
70
+ @result = [result_payload]
71
+ self
72
+ end
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])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ class InventoryAction < Natural20::Action
3
+ def self.can?(_entity, _battle)
4
+ true
5
+ end
6
+
7
+ def build_map
8
+ OpenStruct.new({
9
+ action: self,
10
+ param: [
11
+ {
12
+ type: :show_inventory,
13
+ },
14
+ ],
15
+ next: lambda { |_path|
16
+ OpenStruct.new({
17
+ param: nil,
18
+ next: -> { self },
19
+ })
20
+ },
21
+ })
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ class LookAction < Natural20::Action
2
+ attr_accessor :ui_callback
3
+
4
+ # @param entity [Natural20::Entity]
5
+ # @param battle [Natural20::Battle]
6
+ def self.can?(entity, battle)
7
+ battle.nil? || !battle.ongoing? || battle.entity_state_for(entity)[:active_perception].zero?
8
+ end
9
+
10
+ def build_map
11
+ OpenStruct.new({
12
+ param: [
13
+ {
14
+ type: :look,
15
+ num: 1
16
+ }
17
+ ],
18
+ next: lambda { |callback|
19
+ self.ui_callback = callback
20
+ OpenStruct.new({
21
+ param: nil,
22
+ next: -> { self }
23
+ })
24
+ }
25
+ })
26
+ end
27
+
28
+ def self.build(session, source)
29
+ action = LookAction.new(session, source, :look)
30
+ action.build_map
31
+ end
32
+
33
+ def resolve(_session, _map, opts = {})
34
+ perception_check = @source.perception_check!(opts[:battle])
35
+ perception_check_2 = @source.perception_check!(opts[:battle])
36
+
37
+ perception_check_disadvantage = [perception_check, perception_check_2].min
38
+ @result = [{
39
+ source: @source,
40
+ type: :look,
41
+ die_roll: perception_check,
42
+ die_roll_disadvantage: perception_check_disadvantage,
43
+ battle: opts[:battle]
44
+ }]
45
+ self
46
+ end
47
+
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
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,254 @@
1
+ # typed: true
2
+ class MoveAction < Natural20::Action
3
+ include Natural20::MovementHelper
4
+ include Natural20::ActionDamage
5
+
6
+ attr_accessor :move_path, :jump_index, :as_dash, :as_bonus_action
7
+
8
+ def self.can?(entity, battle)
9
+ battle.nil? || entity.available_movement(battle).positive?
10
+ end
11
+
12
+ def build_map
13
+ OpenStruct.new({
14
+ action: self,
15
+ param: [
16
+ {
17
+ type: :movement
18
+ }
19
+ ],
20
+ next: lambda { |path_and_jump_index|
21
+ path, jump_index = path_and_jump_index
22
+ self.move_path = path
23
+ self.jump_index = jump_index
24
+ OpenStruct.new({
25
+ param: nil,
26
+ next: -> { self }
27
+ })
28
+ }
29
+ })
30
+ end
31
+
32
+ def self.build(session, source)
33
+ action = MoveAction.new(session, source, :move)
34
+ action.build_map
35
+ end
36
+
37
+ # @param map [Natural20::BattleMap]
38
+ # @option opts battle [Natural20::Battle]
39
+ def resolve(_session, map, opts = {})
40
+ raise 'no path specified' if (move_path.nil? || move_path.empty?) && opts[:move_path].nil?
41
+
42
+ @result = []
43
+ # check for melee opportunity attacks
44
+ battle = opts[:battle]
45
+
46
+ current_moves = move_path.presence || opts[:move_path]
47
+ jumps = jump_index || []
48
+
49
+ actual_moves = []
50
+ additional_effects = []
51
+
52
+ movement_budget = if as_dash
53
+ (@source.speed / 5).floor
54
+ else
55
+ (@source.available_movement(battle) / 5).floor
56
+ end
57
+
58
+ movement = compute_actual_moves(@source, current_moves, map, battle, movement_budget, manual_jump: jumps)
59
+ actual_moves = movement.movement
60
+
61
+ actual_moves.pop while actual_moves.last && !map.placeable?(@source, *actual_moves.last, battle)
62
+
63
+ actual_moves = check_opportunity_attacks(@source, actual_moves, battle)
64
+
65
+ # check if movement requires athletics checks
66
+ actual_moves = check_movement_athletics(actual_moves, movement.athletics_check_locations, battle, map)
67
+
68
+ # check if movement requires dex checks, e.g. jumping and landing on difficult terrain
69
+ actual_moves = check_movement_acrobatics(actual_moves, movement.acrobatics_check_locations, battle)
70
+
71
+ # calculate for area based triggers
72
+ cutoff = false
73
+
74
+ safe_moves = []
75
+ actual_moves.each_with_index do |move, _index|
76
+ is_flying_or_jumping = movement.jump_locations.include?(move)
77
+ trigger_results = map.area_trigger!(@source, move, is_flying_or_jumping)
78
+ if trigger_results.empty?
79
+ safe_moves << move
80
+ else
81
+ safe_moves << move
82
+ additional_effects += trigger_results
83
+ break
84
+ end
85
+ end
86
+
87
+ movement = compute_actual_moves(@source, safe_moves, map, battle, movement_budget, manual_jump: jumps)
88
+
89
+ # compute grappled entity movement
90
+ if @source.grappling?
91
+ grappled_movement = movement.movement.dup
92
+ grappled_movement.pop
93
+
94
+ @source.grappling_targets.each do |grappling_target|
95
+ start_pos = map.entity_or_object_pos(grappling_target)
96
+ grappled_entity_movement = [start_pos] + grappled_movement
97
+
98
+ additional_effects << {
99
+ source: grappling_target,
100
+ map: map,
101
+ battle: battle,
102
+ type: :move,
103
+ path: grappled_entity_movement,
104
+ move_cost: 0,
105
+ position: grappled_entity_movement.last
106
+ }
107
+
108
+ grappled_movement.pop
109
+ end
110
+ end
111
+
112
+ @result << {
113
+ source: @source,
114
+ map: map,
115
+ battle: battle,
116
+ type: :move,
117
+ path: movement.movement,
118
+ move_cost: movement_budget - movement.budget,
119
+ position: movement.movement.last
120
+ }
121
+ @result += additional_effects
122
+
123
+ self
124
+ end
125
+
126
+ # @param entity [Natural20::Entity]
127
+ # @param move_list [Array<Array<Integer,Integer>>]
128
+ # @param battle [Natural20::Battle]
129
+ def check_opportunity_attacks(entity, move_list, battle, grappled: false)
130
+ if battle && !@source.disengage?(battle)
131
+ opportunity_attacks = opportunity_attack_list(entity, move_list, battle, battle.map)
132
+ opportunity_attacks.each do |enemy_opporunity|
133
+ next unless enemy_opporunity[:source].has_reaction?(battle)
134
+ next if @source.grappling_targets.include?(enemy_opporunity[:source])
135
+
136
+ original_location = move_list[0...enemy_opporunity[:path]]
137
+ attack_location = original_location.last
138
+ battle.trigger_opportunity_attack(enemy_opporunity[:source], entity, *attack_location)
139
+
140
+ if !grappled && !entity.conscious?
141
+ move_list = original_location
142
+ break
143
+ end
144
+ end
145
+ end
146
+ move_list
147
+ end
148
+
149
+ def opportunity_attack_list(entity, current_moves, battle, map)
150
+ # get opposing forces
151
+ opponents = battle.opponents_of?(entity)
152
+ entered_melee_range = Set.new
153
+ left_melee_range = []
154
+ current_moves.each_with_index do |path, index|
155
+ opponents.each do |enemy|
156
+ entered_melee_range.add(enemy) if enemy.entered_melee?(map, entity, *path)
157
+ if !left_melee_range.include?(enemy) && entered_melee_range.include?(enemy) && !enemy.entered_melee?(map,
158
+ entity, *path)
159
+ left_melee_range << { source: enemy, path: index }
160
+ end
161
+ end
162
+ end
163
+ left_melee_range
164
+ end
165
+
166
+ def apply!(battle)
167
+ @result.each do |item|
168
+ case (item[:type])
169
+ when :state
170
+ item[:params].each do |k, v|
171
+ item[:source].send(:"#{k}=", v)
172
+ end
173
+ when :damage
174
+ damage_event(item, battle)
175
+ when :acrobatics, :athletics
176
+ if item[:success]
177
+ Natural20::EventManager.received_event(source: item[:source], event: item[:type], success: true,
178
+ roll: item[:roll])
179
+ else
180
+ Natural20::EventManager.received_event(source: item[:source], event: item[:type], success: false,
181
+ roll: item[:roll])
182
+ item[:source].prone!
183
+ end
184
+ when :drop_grapple
185
+ item[:target].escape_grapple_from!(@source)
186
+ Natural20::EventManager.received_event(event: :drop_grapple,
187
+ target: item[:target], source: @source,
188
+ source_roll: item[:source_roll],
189
+ target_roll: item[:target_roll])
190
+
191
+ when :move
192
+ item[:map].move_to!(item[:source], *item[:position], battle)
193
+ if as_dash && as_bonus_action
194
+ battle.entity_state_for(item[:source])[:bonus_action] -= 1
195
+ elsif as_dash
196
+ battle.entity_state_for(item[:source])[:action] -= 1
197
+ elsif battle
198
+ battle.entity_state_for(item[:source])[:movement] -= item[:move_cost] * battle.map.feet_per_grid
199
+ end
200
+
201
+ Natural20::EventManager.received_event({ event: :move, source: item[:source], position: item[:position], path: item[:path],
202
+ feet_per_grid: battle.map&.feet_per_grid,
203
+ as_dash: as_dash, as_bonus: as_bonus_action })
204
+ end
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ # @param actual_moves [Array]
211
+ # @param dexterity_checks [Array]
212
+ # @param battle [Natural20::Battle]
213
+ # @return [Array]
214
+ def check_movement_acrobatics(actual_moves, dexterity_checks, battle)
215
+ cutoff = actual_moves.size - 1
216
+ actual_moves.each_with_index do |m, index|
217
+ next unless dexterity_checks.include?(m)
218
+
219
+ acrobatics_roll = @source.acrobatics_check!(battle)
220
+ if acrobatics_roll.result >= 10
221
+ @result << { source: @source, type: :acrobatics, success: true, roll: acrobatics_roll, location: m }
222
+ else
223
+ @result << { source: @source, type: :acrobatics, success: false, roll: acrobatics_roll, location: m }
224
+ cutoff = index
225
+ break
226
+ end
227
+ end
228
+ actual_moves[0..cutoff]
229
+ end
230
+
231
+ # @param actual_moves [Array]
232
+ # @param athletics_checks [Array]
233
+ # @param battle [Natural20::Battle]
234
+ # @param map [Natural20::BattleMap]
235
+ # @return [Array]
236
+ def check_movement_athletics(actual_moves, athletics_checks, battle, map)
237
+ cutoff = actual_moves.size - 1
238
+ actual_moves.each_with_index do |m, index|
239
+ next unless athletics_checks.include?(m)
240
+
241
+ athletics_roll = @source.athletics_check!(battle)
242
+ if athletics_roll.result >= 10
243
+ @result << { source: @source, type: :athletics, success: true, roll: athletics_roll, location: m }
244
+ else
245
+ @result << { source: @source, type: :athletics, success: false, roll: athletics_roll, location: m }
246
+ cutoff = index - 1
247
+ cutoff -= 1 while cutoff >= 0 && !map.placeable?(@source, *actual_moves[cutoff], battle)
248
+ break
249
+ end
250
+ end
251
+
252
+ actual_moves[0..cutoff]
253
+ end
254
+ end