natural_20 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/compute_lights +19 -0
- data/bin/console +19 -0
- data/bin/nat20 +135 -0
- data/bin/nat20.cmd +3 -0
- data/bin/nat20author +104 -0
- data/bin/setup +8 -0
- data/char_classes/fighter.yml +45 -0
- data/char_classes/rogue.yml +54 -0
- data/characters/halfling_rogue.yml +46 -0
- data/characters/high_elf_fighter.yml +49 -0
- data/fixtures/battle_sim.yml +58 -0
- data/fixtures/battle_sim_2.yml +30 -0
- data/fixtures/battle_sim_3.yml +26 -0
- data/fixtures/battle_sim_4.yml +26 -0
- data/fixtures/battle_sim_objects.yml +101 -0
- data/fixtures/corridors.yml +24 -0
- data/fixtures/elf_rogue.yml +39 -0
- data/fixtures/halfling_rogue.yml +41 -0
- data/fixtures/high_elf_fighter.yml +49 -0
- data/fixtures/human_fighter.yml +48 -0
- data/fixtures/path_finding_test.yml +11 -0
- data/fixtures/path_finding_test_2.yml +15 -0
- data/fixtures/path_finding_test_3.yml +26 -0
- data/fixtures/thin_walls.yml +53 -0
- data/fixtures/traps.yml +25 -0
- data/game.yml +20 -0
- data/items/equipment.yml +101 -0
- data/items/objects.yml +73 -0
- data/items/weapons.yml +297 -0
- data/lib/natural_20.rb +68 -0
- data/lib/natural_20/actions/action.rb +40 -0
- data/lib/natural_20/actions/attack_action.rb +372 -0
- data/lib/natural_20/actions/concerns/action_damage.rb +14 -0
- data/lib/natural_20/actions/dash_action.rb +46 -0
- data/lib/natural_20/actions/disengage_action.rb +53 -0
- data/lib/natural_20/actions/dodge_action.rb +45 -0
- data/lib/natural_20/actions/escape_grapple_action.rb +97 -0
- data/lib/natural_20/actions/first_aid_action.rb +109 -0
- data/lib/natural_20/actions/grapple_action.rb +185 -0
- data/lib/natural_20/actions/ground_interact_action.rb +74 -0
- data/lib/natural_20/actions/help_action.rb +56 -0
- data/lib/natural_20/actions/hide_action.rb +53 -0
- data/lib/natural_20/actions/interact_action.rb +91 -0
- data/lib/natural_20/actions/inventory_action.rb +23 -0
- data/lib/natural_20/actions/look_action.rb +63 -0
- data/lib/natural_20/actions/move_action.rb +254 -0
- data/lib/natural_20/actions/multiattack_action.rb +41 -0
- data/lib/natural_20/actions/prone_action.rb +38 -0
- data/lib/natural_20/actions/short_rest_action.rb +53 -0
- data/lib/natural_20/actions/shove_action.rb +142 -0
- data/lib/natural_20/actions/stand_action.rb +47 -0
- data/lib/natural_20/actions/use_item_action.rb +57 -0
- data/lib/natural_20/ai_controller/path_compute.rb +140 -0
- data/lib/natural_20/ai_controller/standard.rb +288 -0
- data/lib/natural_20/battle.rb +544 -0
- data/lib/natural_20/battle_map.rb +843 -0
- data/lib/natural_20/cli/builder/fighter_builder.rb +104 -0
- data/lib/natural_20/cli/builder/rogue_builder.rb +62 -0
- data/lib/natural_20/cli/character_builder.rb +210 -0
- data/lib/natural_20/cli/commandline_ui.rb +612 -0
- data/lib/natural_20/cli/inventory_ui.rb +136 -0
- data/lib/natural_20/cli/map_renderer.rb +165 -0
- data/lib/natural_20/concerns/container.rb +32 -0
- data/lib/natural_20/concerns/entity.rb +1213 -0
- data/lib/natural_20/concerns/evaluator/entity_state_evaluator.rb +59 -0
- data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +51 -0
- data/lib/natural_20/concerns/fighter_class.rb +35 -0
- data/lib/natural_20/concerns/health_flavor.rb +27 -0
- data/lib/natural_20/concerns/lootable.rb +94 -0
- data/lib/natural_20/concerns/movement_helper.rb +195 -0
- data/lib/natural_20/concerns/multiattack.rb +54 -0
- data/lib/natural_20/concerns/navigation.rb +87 -0
- data/lib/natural_20/concerns/notable.rb +37 -0
- data/lib/natural_20/concerns/rogue_class.rb +26 -0
- data/lib/natural_20/controller.rb +11 -0
- data/lib/natural_20/die_roll.rb +331 -0
- data/lib/natural_20/event_manager.rb +288 -0
- data/lib/natural_20/item_library/base_item.rb +27 -0
- data/lib/natural_20/item_library/chest.rb +230 -0
- data/lib/natural_20/item_library/door_object.rb +189 -0
- data/lib/natural_20/item_library/ground.rb +124 -0
- data/lib/natural_20/item_library/healing_potion.rb +51 -0
- data/lib/natural_20/item_library/object.rb +153 -0
- data/lib/natural_20/item_library/pit_trap.rb +69 -0
- data/lib/natural_20/item_library/stone_wall.rb +18 -0
- data/lib/natural_20/npc.rb +173 -0
- data/lib/natural_20/player_character.rb +414 -0
- data/lib/natural_20/session.rb +168 -0
- data/lib/natural_20/utils/cover.rb +35 -0
- data/lib/natural_20/utils/ray_tracer.rb +90 -0
- data/lib/natural_20/utils/static_light_builder.rb +72 -0
- data/lib/natural_20/utils/weapons.rb +78 -0
- data/lib/natural_20/version.rb +4 -0
- data/locales/en.yml +304 -0
- data/maps/game_map.yml +168 -0
- data/natural_20.gemspec +46 -0
- data/npcs/goblin.yml +64 -0
- data/npcs/human_guard.yml +48 -0
- data/npcs/ogre.yml +61 -0
- data/npcs/owlbear.yml +55 -0
- data/npcs/wolf.yml +46 -0
- data/races/elf.yml +44 -0
- data/races/halfling.yml +22 -0
- data/races/human.yml +13 -0
- 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
|