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,41 @@
|
|
1
|
+
# typed: true
|
2
|
+
class MultiattackAction < Natural20::Action
|
3
|
+
attr_accessor :as_bonus_action
|
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
|
+
param: nil,
|
12
|
+
next: -> { self },
|
13
|
+
})
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build(session, source)
|
17
|
+
action = MultiattackAction.new(session, source, :multiattack)
|
18
|
+
action.build_map
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve(_session, _map, opts = {})
|
22
|
+
@result = [{
|
23
|
+
source: @source,
|
24
|
+
type: :multiattack,
|
25
|
+
battle: opts[:battle],
|
26
|
+
}]
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param battle [Natural20::Battle]
|
31
|
+
def apply!(battle)
|
32
|
+
@result.each do |item|
|
33
|
+
case (item[:type])
|
34
|
+
when :multiattack
|
35
|
+
@total_attacks += 2
|
36
|
+
end
|
37
|
+
|
38
|
+
battle.consume!(:action, 1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: true
|
2
|
+
class ProneAction < Natural20::Action
|
3
|
+
attr_accessor :as_bonus_action
|
4
|
+
|
5
|
+
def self.can?(entity, battle)
|
6
|
+
battle && !entity.prone?
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_map
|
10
|
+
OpenStruct.new({
|
11
|
+
param: nil,
|
12
|
+
next: -> { self }
|
13
|
+
})
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build(session, source)
|
17
|
+
action = ProneAction.new(session, source, :attack)
|
18
|
+
action.build_map
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve(_session, _map, opts = {})
|
22
|
+
@result = [{
|
23
|
+
source: @source,
|
24
|
+
type: :prone,
|
25
|
+
battle: opts[:battle]
|
26
|
+
}]
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply!(_battle)
|
31
|
+
@result.each do |item|
|
32
|
+
case (item[:type])
|
33
|
+
when :prone
|
34
|
+
item[:source].prone!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class ShortRestAction < Natural20::Action
|
2
|
+
attr_accessor :as_bonus_action
|
3
|
+
|
4
|
+
# @param entity [Natural20::Entity]
|
5
|
+
# @param battle [Natural20::Battle]
|
6
|
+
def self.can?(entity, battle)
|
7
|
+
battle && !battle.combat? && (entity.conscious? || entity.stable?)
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_map
|
11
|
+
OpenStruct.new({
|
12
|
+
param: nil,
|
13
|
+
next: -> { self }
|
14
|
+
})
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.build(session, source)
|
18
|
+
action = ShortRestAction.new(session, source, :short_rest)
|
19
|
+
action.build_map
|
20
|
+
end
|
21
|
+
|
22
|
+
# @option opts battle [Natural20::Battle]
|
23
|
+
def resolve(_session, _map, opts = {})
|
24
|
+
battle = opts[:battle]
|
25
|
+
|
26
|
+
# determine who in the party is eligible for short rest
|
27
|
+
entities = battle.current_party.select do |entity|
|
28
|
+
entity.conscious? || entity.stable?
|
29
|
+
end
|
30
|
+
|
31
|
+
@result = [{
|
32
|
+
source: @source,
|
33
|
+
targets: entities,
|
34
|
+
type: :short_rest,
|
35
|
+
battle: battle
|
36
|
+
}]
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param battle [Natural20::Battle]
|
42
|
+
def apply!(battle)
|
43
|
+
@result.each do |item|
|
44
|
+
case (item[:type])
|
45
|
+
when :short_rest
|
46
|
+
Natural20::EventManager.received_event({ source: item[:source], event: :short_rest, targets: item[:targets] })
|
47
|
+
item[:targets].each do |entity|
|
48
|
+
entity.short_rest!(battle, prompt: true)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
class ShoveAction < Natural20::Action
|
2
|
+
include Natural20::ActionDamage
|
3
|
+
attr_accessor :target, :knock_prone
|
4
|
+
|
5
|
+
def self.can?(entity, battle, _options = {})
|
6
|
+
(battle.nil? || entity.total_actions(battle).positive?)
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate
|
10
|
+
@errors << 'target is a required option for :attack' if target.nil?
|
11
|
+
@errors << t('validation.shove.invalid_target_size') if (target.size_identifier - @source.size_identifier) > 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@action_type.to_s.humanize
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_map
|
19
|
+
OpenStruct.new({
|
20
|
+
action: self,
|
21
|
+
param: [
|
22
|
+
{
|
23
|
+
type: :select_target,
|
24
|
+
range: 5,
|
25
|
+
target_types: %i[enemies],
|
26
|
+
num: 1
|
27
|
+
}
|
28
|
+
],
|
29
|
+
next: lambda { |target|
|
30
|
+
self.target = target
|
31
|
+
OpenStruct.new({
|
32
|
+
param: nil,
|
33
|
+
next: -> { self }
|
34
|
+
})
|
35
|
+
}
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.build(session, source)
|
40
|
+
action = ShoveAction.new(session, source, :shove)
|
41
|
+
action.build_map
|
42
|
+
end
|
43
|
+
|
44
|
+
# Build the attack roll information
|
45
|
+
# @param session [Natural20::Session]
|
46
|
+
# @param map [Natural20::BattleMap]
|
47
|
+
# @option opts battle [Natural20::Battle]
|
48
|
+
# @option opts target [Natural20::Entity]
|
49
|
+
def resolve(_session, map, opts = {})
|
50
|
+
target = opts[:target] || @target
|
51
|
+
battle = opts[:battle]
|
52
|
+
raise 'target is a required option for :attack' if target.nil?
|
53
|
+
|
54
|
+
return if (target.size_identifier - @source.size_identifier) > 1
|
55
|
+
|
56
|
+
strength_roll = @source.athletics_check!(battle)
|
57
|
+
athletics_stats = (@target.athletics_proficient? ? @target.proficiency_bonus : 0) + @target.str_mod
|
58
|
+
acrobatics_stats = (@target.acrobatics_proficient? ? @target.proficiency_bonus : 0) + @target.dex_mod
|
59
|
+
|
60
|
+
shove_success = false
|
61
|
+
if @target.incapacitated?
|
62
|
+
shove_success = true
|
63
|
+
else
|
64
|
+
contested_roll = if athletics_stats > acrobatics_stats
|
65
|
+
@target.athletics_check!(battle,
|
66
|
+
description: t('die_roll.contest'))
|
67
|
+
else
|
68
|
+
@target.acrobatics_check!(
|
69
|
+
opts[:battle], description: t('die_roll.contest')
|
70
|
+
)
|
71
|
+
end
|
72
|
+
shove_success = strength_roll.result >= contested_roll.result
|
73
|
+
end
|
74
|
+
|
75
|
+
shove_loc = nil
|
76
|
+
additional_effects = []
|
77
|
+
unless knock_prone
|
78
|
+
shove_loc = @target.push_from(map, *map.entity_or_object_pos(@source))
|
79
|
+
if shove_loc
|
80
|
+
trigger_results = map.area_trigger!(@target, shove_loc, false)
|
81
|
+
additional_effects += trigger_results
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@result = if shove_success
|
85
|
+
[{
|
86
|
+
source: @source,
|
87
|
+
target: target,
|
88
|
+
type: :shove,
|
89
|
+
success: true,
|
90
|
+
battle: battle,
|
91
|
+
shove_loc: shove_loc,
|
92
|
+
knock_prone: knock_prone,
|
93
|
+
source_roll: strength_roll,
|
94
|
+
target_roll: contested_roll
|
95
|
+
}] + additional_effects
|
96
|
+
else
|
97
|
+
[{
|
98
|
+
source: @source,
|
99
|
+
target: target,
|
100
|
+
type: :shove,
|
101
|
+
success: false,
|
102
|
+
battle: battle,
|
103
|
+
knock_prone: knock_prone,
|
104
|
+
source_roll: strength_roll,
|
105
|
+
target_roll: contested_roll
|
106
|
+
}]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def apply!(battle)
|
111
|
+
@result.each do |item|
|
112
|
+
case (item[:type])
|
113
|
+
when :damage
|
114
|
+
damage_event(item, battle)
|
115
|
+
when :shove
|
116
|
+
if item[:success]
|
117
|
+
if item[:knock_prone]
|
118
|
+
item[:target].prone!
|
119
|
+
elsif item[:shove_loc]
|
120
|
+
item[:battle].map.move_to!(item[:target], *item[:shove_loc], battle)
|
121
|
+
end
|
122
|
+
|
123
|
+
Natural20::EventManager.received_event(event: :shove_success,
|
124
|
+
knock_prone: item[:knock_prone],
|
125
|
+
target: item[:target], source: item[:source],
|
126
|
+
source_roll: item[:source_roll],
|
127
|
+
target_roll: item[:target_roll])
|
128
|
+
else
|
129
|
+
Natural20::EventManager.received_event(event: :shove_failure,
|
130
|
+
target: item[:target], source: item[:source],
|
131
|
+
source_roll: item[:source_roll],
|
132
|
+
target_roll: item[:target_roll])
|
133
|
+
end
|
134
|
+
|
135
|
+
battle.entity_state_for(item[:source])[:action] -= 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class PushAction < ShoveAction
|
142
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# typed: true
|
2
|
+
class StandAction < Natural20::Action
|
3
|
+
attr_accessor :as_bonus_action
|
4
|
+
|
5
|
+
# @param entity [Natural20::Entity]
|
6
|
+
# @param battle [Natural20::Battle]
|
7
|
+
def self.can?(entity, battle)
|
8
|
+
battle && entity.prone? && entity.speed.positive? && entity.available_movement(battle) >= required_movement(entity)
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_map
|
12
|
+
OpenStruct.new({
|
13
|
+
param: nil,
|
14
|
+
next: -> { self }
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.build(session, source)
|
19
|
+
action = StandAction.new(session, source, :attack)
|
20
|
+
action.build_map
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve(_session, _map, opts = {})
|
24
|
+
@result = [{
|
25
|
+
source: @source,
|
26
|
+
type: :stand,
|
27
|
+
battle: opts[:battle]
|
28
|
+
}]
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def apply!(battle)
|
33
|
+
@result.each do |item|
|
34
|
+
case (item[:type])
|
35
|
+
when :stand
|
36
|
+
item[:source].stand!
|
37
|
+
battle.entity_state_for(item[:source])[:movement] -= (item[:source].speed / 2).floor
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# required movement available to stand
|
43
|
+
# @param entity [Natural20::Entity]
|
44
|
+
def self.required_movement(entity)
|
45
|
+
(entity.speed / 2).floor
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# typed: true
|
2
|
+
class UseItemAction < Natural20::Action
|
3
|
+
attr_accessor :target, :target_item
|
4
|
+
|
5
|
+
def self.can?(entity, battle)
|
6
|
+
battle.nil? || entity.total_actions(battle).positive?
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.build(session, source)
|
10
|
+
action = UseItemAction.new(session, source, :attack)
|
11
|
+
action.build_map
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_map
|
15
|
+
OpenStruct.new({
|
16
|
+
action: self,
|
17
|
+
param: [
|
18
|
+
{
|
19
|
+
type: :select_item
|
20
|
+
}
|
21
|
+
],
|
22
|
+
next: lambda { |item|
|
23
|
+
item_details = session.load_equipment(item)
|
24
|
+
raise "item #{item_details[:name]} not usable!" unless item_details[:usable]
|
25
|
+
|
26
|
+
@target_item = item_details[:item_class].constantize.new(item, item_details)
|
27
|
+
@target_item.build_map(self)
|
28
|
+
}
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolve(_session, map = nil, opts = {})
|
33
|
+
battle = opts[:battle]
|
34
|
+
result_payload = {
|
35
|
+
source: @source,
|
36
|
+
target: target,
|
37
|
+
map: map,
|
38
|
+
battle: battle,
|
39
|
+
type: :use_item,
|
40
|
+
item: target_item
|
41
|
+
}.merge(target_item.resolve(@source, battle))
|
42
|
+
@result = [result_payload]
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply!(battle)
|
47
|
+
@result.each do |item|
|
48
|
+
case (item[:type])
|
49
|
+
when :use_item
|
50
|
+
Natural20::EventManager.received_event({ event: :use_item, source: item[:source], item: item[:item] })
|
51
|
+
item[:item].use!(item[:target], item)
|
52
|
+
item[:source].deduct_item(item[:item].name, 1) if item[:item].consumable?
|
53
|
+
battle.entity_state_for(item[:source])[:action] -= 1 if battle
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# typed: false
|
2
|
+
require 'pqueue'
|
3
|
+
module AiController
|
4
|
+
MAX_DISTANCE = 4_000_000
|
5
|
+
# Path finding algorithm
|
6
|
+
class PathCompute
|
7
|
+
# Creates a path finder
|
8
|
+
# @param battle [Natural20::Battle]
|
9
|
+
# @param map [Natural20::BattleMap]
|
10
|
+
# @param entity [Natural20::Entity]
|
11
|
+
def initialize(battle, map, entity)
|
12
|
+
@entity = entity
|
13
|
+
@map = map
|
14
|
+
@battle = battle
|
15
|
+
@max_x, @max_y = @map.size
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_structures(source_x, source_y)
|
19
|
+
@pq = PQueue.new([]) { |a, b| a[1] < b[1] }
|
20
|
+
@visited_nodes = Set.new
|
21
|
+
|
22
|
+
@distances = @max_x.times.map do
|
23
|
+
@max_y.times.map do
|
24
|
+
MAX_DISTANCE
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@current_node = [source_x, source_y]
|
29
|
+
@distances[source_x][source_y] = 0
|
30
|
+
@visited_nodes.add(@current_node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def backtrace(source_x, source_y, destination_x, destination_y, show_cost: false)
|
34
|
+
path = []
|
35
|
+
current_node = [destination_x, destination_y]
|
36
|
+
return nil if @distances[destination_x][destination_y] == MAX_DISTANCE # no route!
|
37
|
+
|
38
|
+
path << current_node
|
39
|
+
cost = @distances[destination_x][destination_y]
|
40
|
+
visited_nodes = Set.new
|
41
|
+
visited_nodes.add(current_node)
|
42
|
+
Kernel.loop do
|
43
|
+
adjacent_squares = get_adjacent_from(*current_node)
|
44
|
+
adjacent_squares += get_adjacent_from(*current_node, squeeze: true)
|
45
|
+
|
46
|
+
min_node = nil
|
47
|
+
min_distance = nil
|
48
|
+
|
49
|
+
adjacent_squares.reject { |n| visited_nodes.include?(n) }.each do |node|
|
50
|
+
line_distance = Math.sqrt((destination_x - node[0])**2 + (destination_y - node[1])**2)
|
51
|
+
current_distance = @distances[node[0]][node[1]].to_f + line_distance / MAX_DISTANCE.to_f
|
52
|
+
if min_node.nil? || current_distance < min_distance
|
53
|
+
min_distance = current_distance
|
54
|
+
min_node = node
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return nil if min_node.nil?
|
59
|
+
|
60
|
+
path << min_node
|
61
|
+
current_node = min_node
|
62
|
+
visited_nodes.add(current_node)
|
63
|
+
break if current_node == [source_x, source_y]
|
64
|
+
end
|
65
|
+
|
66
|
+
show_cost ? [path.reverse, cost] : path.reverse
|
67
|
+
end
|
68
|
+
|
69
|
+
def path(destination = nil)
|
70
|
+
Kernel.loop do
|
71
|
+
distance = @distances[@current_node[0]][@current_node[1]]
|
72
|
+
|
73
|
+
adjacent_squares = get_adjacent_from(*@current_node)
|
74
|
+
visit_squares(@pq, adjacent_squares, @visited_nodes, @distances, distance)
|
75
|
+
|
76
|
+
# with squeezing into terrain
|
77
|
+
squeeze_adjacent_squares = get_adjacent_from(*@current_node, squeeze: true)
|
78
|
+
|
79
|
+
squeeze_adjacent_squares -= adjacent_squares
|
80
|
+
|
81
|
+
unless squeeze_adjacent_squares.empty?
|
82
|
+
visit_squares(@pq, squeeze_adjacent_squares, @visited_nodes, @distances, distance,
|
83
|
+
2)
|
84
|
+
end
|
85
|
+
|
86
|
+
break if destination && @current_node == destination
|
87
|
+
|
88
|
+
@visited_nodes.add(@current_node)
|
89
|
+
|
90
|
+
@current_node, node_d = @pq.pop
|
91
|
+
break if @current_node.nil?
|
92
|
+
|
93
|
+
return nil if node_d == MAX_DISTANCE
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# compute path using Djikstras shortest path
|
98
|
+
def compute_path(source_x, source_y, destination_x, destination_y)
|
99
|
+
build_structures(source_x, source_y)
|
100
|
+
path([destination_x, destination_y])
|
101
|
+
backtrace(source_x, source_y, destination_x, destination_y)
|
102
|
+
end
|
103
|
+
|
104
|
+
def incremental_path(source_x, source_y, destination_x, destination_y)
|
105
|
+
rpath = backtrace(source_x, source_y, destination_x, destination_y, show_cost: true)
|
106
|
+
return rpath if rpath && rpath.size > 1
|
107
|
+
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param pq [PQueue]
|
112
|
+
# @param adjacent_squares [Array<Array<Integer,Integer>>]
|
113
|
+
def visit_squares(pq, adjacent_squares, visited_nodes, distances, distance, override_move_cost = nil)
|
114
|
+
adjacent_squares.reject { |n| visited_nodes.include?(n) }.each do |node|
|
115
|
+
move_cost = override_move_cost || @map.difficult_terrain?(@entity, node[0], node[1], @battle) ? 2 : 1
|
116
|
+
current_distance = distance + move_cost
|
117
|
+
distances[node[0]][node[1]] = current_distance if distances[node[0]][node[1]] > current_distance
|
118
|
+
pq << [node, distances[node[0]][node[1]]]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_adjacent_from(pos_x, pos_y, squeeze: false)
|
123
|
+
valid_paths = Set.new
|
124
|
+
[-1, 0, 1].each do |x_op|
|
125
|
+
[-1, 0, 1].each do |y_op|
|
126
|
+
cur_x = pos_x + x_op
|
127
|
+
cur_y = pos_y + y_op
|
128
|
+
|
129
|
+
next if cur_x < 0 || cur_y < 0 || cur_x >= @max_x || cur_y >= @max_y
|
130
|
+
next if x_op.zero? && y_op.zero?
|
131
|
+
next unless @map.passable?(@entity, cur_x, cur_y, @battle, squeeze)
|
132
|
+
|
133
|
+
valid_paths.add([cur_x, cur_y])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
valid_paths
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|