natural_20 0.1.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.
- 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,69 @@
|
|
1
|
+
module ItemLibrary
|
2
|
+
# Represents a staple of DnD the concealed pit trap
|
3
|
+
class PitTrap < Object
|
4
|
+
include AreaTrigger
|
5
|
+
|
6
|
+
attr_accessor :activated
|
7
|
+
|
8
|
+
def area_trigger_handler(entity, entity_pos, is_flying)
|
9
|
+
result = []
|
10
|
+
return nil if entity_pos != position
|
11
|
+
return nil if is_flying
|
12
|
+
|
13
|
+
unless activated
|
14
|
+
damage = Natural20::DieRoll.roll(@properties[:damage_die])
|
15
|
+
result = [
|
16
|
+
{
|
17
|
+
source: self,
|
18
|
+
type: :state,
|
19
|
+
params: {
|
20
|
+
activated: true
|
21
|
+
}
|
22
|
+
},
|
23
|
+
{
|
24
|
+
source: self,
|
25
|
+
target: entity,
|
26
|
+
type: :damage,
|
27
|
+
attack_name: @properties[:attack_name] || 'pit trap',
|
28
|
+
damage_type: @properties[:damage_type] || 'piercing',
|
29
|
+
damage: damage
|
30
|
+
}
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def placeable?
|
38
|
+
!activated
|
39
|
+
end
|
40
|
+
|
41
|
+
def label
|
42
|
+
return 'ground' unless activated
|
43
|
+
|
44
|
+
@properties[:name].presence || 'pit trap'
|
45
|
+
end
|
46
|
+
|
47
|
+
def passable?
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def token
|
52
|
+
["\u02ac"]
|
53
|
+
end
|
54
|
+
|
55
|
+
def concealed?
|
56
|
+
!activated
|
57
|
+
end
|
58
|
+
|
59
|
+
def jump_required?
|
60
|
+
activated
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def setup_other_attributes
|
66
|
+
@activated = false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# typed: false
|
2
|
+
require "random_name_generator"
|
3
|
+
|
4
|
+
module Natural20
|
5
|
+
class Npc
|
6
|
+
include Natural20::Entity
|
7
|
+
include Natural20::Notable
|
8
|
+
prepend Natural20::Lootable
|
9
|
+
include Natural20::HealthFlavor
|
10
|
+
include Multiattack
|
11
|
+
|
12
|
+
attr_accessor :hp, :resistances, :npc_actions, :battle_defaults, :npc_type
|
13
|
+
|
14
|
+
# @param session [Session]
|
15
|
+
# @param type [String,Symbol]
|
16
|
+
# @option opt rand_life [Boolean] Determines if will use die for npc HP instead of fixed value
|
17
|
+
def initialize(session, type, opt = {})
|
18
|
+
@properties = YAML.load_file(File.join("npcs", "#{type}.yml")).deep_symbolize_keys!
|
19
|
+
@properties.merge!(opt[:overrides].presence || {})
|
20
|
+
@ability_scores = @properties[:ability]
|
21
|
+
@color = @properties[:color]
|
22
|
+
@session = session
|
23
|
+
@npc_type = type
|
24
|
+
@inventory = @properties[:default_inventory]&.map do |inventory|
|
25
|
+
[inventory[:type].to_sym, OpenStruct.new({ qty: inventory[:qty] })]
|
26
|
+
end.to_h || {}
|
27
|
+
|
28
|
+
@properties[:inventory]&.each do |inventory|
|
29
|
+
@inventory[inventory[:type].to_sym] = OpenStruct.new({ qty: inventory[:qty] })
|
30
|
+
end
|
31
|
+
|
32
|
+
@npc_actions = @properties[:actions]
|
33
|
+
@battle_defaults = @properties[:battle_defaults]
|
34
|
+
@opt = opt
|
35
|
+
@resistances = []
|
36
|
+
@statuses = Set.new
|
37
|
+
|
38
|
+
@properties[:statuses]&.each do |stat|
|
39
|
+
@statuses.add(stat.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
name = case type
|
43
|
+
when "goblin"
|
44
|
+
RandomNameGenerator.new(RandomNameGenerator::GOBLIN).compose(1)
|
45
|
+
when "ogre"
|
46
|
+
%w[Guzar Irth Grukurg Zoduk].sample(1).first
|
47
|
+
else
|
48
|
+
type.to_s.humanize
|
49
|
+
end
|
50
|
+
@name = opt.fetch(:name, name)
|
51
|
+
@entity_uid = SecureRandom.uuid
|
52
|
+
setup_attributes
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :name
|
56
|
+
|
57
|
+
def kind
|
58
|
+
@properties[:kind]
|
59
|
+
end
|
60
|
+
|
61
|
+
def size
|
62
|
+
@properties[:size]
|
63
|
+
end
|
64
|
+
|
65
|
+
def token
|
66
|
+
@properties[:token]
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_reader :name
|
70
|
+
|
71
|
+
attr_reader :max_hp
|
72
|
+
|
73
|
+
def npc?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def armor_class
|
78
|
+
@properties[:default_ac]
|
79
|
+
end
|
80
|
+
|
81
|
+
def speed
|
82
|
+
@properties[:speed]
|
83
|
+
end
|
84
|
+
|
85
|
+
def available_actions(session, battle, opportunity_attack: false)
|
86
|
+
return %i[end] if unconscious?
|
87
|
+
|
88
|
+
if opportunity_attack
|
89
|
+
return generate_npc_attack_actions(battle, opportunity_attack: true).select do |s|
|
90
|
+
s.action_type == :attack && s.npc_action[:type] == 'melee_attack'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
[ generate_npc_attack_actions(battle) +
|
95
|
+
|
96
|
+
%i[hide dodge look stand move dash grapple escape_grapple].map do |type|
|
97
|
+
next unless "#{type.to_s.camelize}Action".constantize.can?(self, battle)
|
98
|
+
case type
|
99
|
+
when :dodge
|
100
|
+
DodgeAction.new(session, self, :dodge)
|
101
|
+
when :hide
|
102
|
+
HideAction.new(session, self, :hide)
|
103
|
+
when :disengage
|
104
|
+
action = DisengageAction.new(session, self, :disengage)
|
105
|
+
action
|
106
|
+
when :move
|
107
|
+
MoveAction.new(session, self, type)
|
108
|
+
when :stand
|
109
|
+
StandAction.new(session, self, type)
|
110
|
+
when :dash
|
111
|
+
action = DashAction.new(session, self, type)
|
112
|
+
action
|
113
|
+
when :help
|
114
|
+
action = HelpAction.new(session, self, :help)
|
115
|
+
action
|
116
|
+
else
|
117
|
+
Natural20::Action.new(session, self, type)
|
118
|
+
end
|
119
|
+
end.compact].flatten
|
120
|
+
end
|
121
|
+
|
122
|
+
def melee_distance
|
123
|
+
@properties[:actions].select { |a| a[:type] == "melee_attack" }.map do |action|
|
124
|
+
action[:range]
|
125
|
+
end&.max
|
126
|
+
end
|
127
|
+
|
128
|
+
def class_feature?(feature)
|
129
|
+
@properties[:attributes]&.include?(feature)
|
130
|
+
end
|
131
|
+
|
132
|
+
def available_interactions(entity, battle)
|
133
|
+
[]
|
134
|
+
end
|
135
|
+
|
136
|
+
def proficient_with_equipped_armor?
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_npc_attack_actions(battle, opportunity_attack: false)
|
141
|
+
actions = []
|
142
|
+
|
143
|
+
actions += npc_actions.map do |npc_action|
|
144
|
+
next if npc_action[:ammo] && item_count(npc_action[:ammo]) <= 0
|
145
|
+
next if npc_action[:if] && !eval_if(npc_action[:if])
|
146
|
+
next unless AttackAction.can?(self, battle, npc_action: npc_action, opportunity_attack: opportunity_attack)
|
147
|
+
|
148
|
+
action = AttackAction.new(session, self, :attack)
|
149
|
+
|
150
|
+
action.npc_action = npc_action
|
151
|
+
action
|
152
|
+
end.compact
|
153
|
+
|
154
|
+
actions
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def setup_attributes
|
160
|
+
super
|
161
|
+
|
162
|
+
@max_hp = @opt[:rand_life] ? Natural20::DieRoll.roll(@properties[:hp_die]).result : @properties[:max_hp]
|
163
|
+
@hp = [@properties.fetch(:override_hp, @max_hp), @max_hp].min
|
164
|
+
|
165
|
+
# parse hit die details
|
166
|
+
hp_details = Natural20::DieRoll.parse(@properties[:hp_die] || "1d6")
|
167
|
+
@max_hit_die = {}
|
168
|
+
@current_hit_die = {}
|
169
|
+
@max_hit_die[npc_type] = hp_details.die_count
|
170
|
+
@current_hit_die[hp_details.die_type.to_i] = hp_details.die_count
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,414 @@
|
|
1
|
+
# typed: false
|
2
|
+
module Natural20
|
3
|
+
class PlayerCharacter
|
4
|
+
include Natural20::Entity
|
5
|
+
include Natural20::RogueClass
|
6
|
+
include Natural20::FighterClass
|
7
|
+
include Natural20::HealthFlavor
|
8
|
+
prepend Natural20::Lootable
|
9
|
+
include Multiattack
|
10
|
+
|
11
|
+
attr_accessor :hp, :other_counters, :resistances, :experience_points, :class_properties
|
12
|
+
|
13
|
+
ACTION_LIST = %i[first_aid look attack move dash hide help dodge disengage use_item interact ground_interact inventory disengage_bonus
|
14
|
+
dash_bonus hide_bonus grapple escape_grapple drop_grapple shove push prone stand short_rest two_weapon_attack].freeze
|
15
|
+
|
16
|
+
# @param session [Natural20::Session]
|
17
|
+
def initialize(session, properties)
|
18
|
+
@session = session
|
19
|
+
@properties = properties.deep_symbolize_keys!
|
20
|
+
|
21
|
+
@ability_scores = @properties[:ability]
|
22
|
+
@equipped = @properties[:equipped]
|
23
|
+
@race_properties = YAML.load_file(File.join(session.root_path, 'races',
|
24
|
+
"#{@properties[:race]}.yml")).deep_symbolize_keys!
|
25
|
+
@inventory = {}
|
26
|
+
@color = @properties[:color]
|
27
|
+
@properties[:inventory]&.each do |inventory|
|
28
|
+
@inventory[inventory[:type].to_sym] ||= OpenStruct.new({ type: inventory[:type], qty: 0 })
|
29
|
+
@inventory[inventory[:type].to_sym].qty += inventory[:qty]
|
30
|
+
end
|
31
|
+
@statuses = Set.new
|
32
|
+
@resistances = []
|
33
|
+
entity_uid = SecureRandom.uuid
|
34
|
+
setup_attributes
|
35
|
+
@max_hit_die = {}
|
36
|
+
@current_hit_die = {}
|
37
|
+
|
38
|
+
@class_properties = @properties[:classes].map do |klass, level|
|
39
|
+
send(:"#{klass}_level=", level)
|
40
|
+
send(:"initialize_#{klass}")
|
41
|
+
|
42
|
+
@max_hit_die[klass] = level
|
43
|
+
|
44
|
+
character_class_properties =
|
45
|
+
YAML.load_file(File.join(session.root_path, 'char_classes', "#{klass}.yml")).deep_symbolize_keys!
|
46
|
+
hit_die_details = DieRoll.parse(character_class_properties[:hit_die])
|
47
|
+
@current_hit_die[hit_die_details.die_type.to_i] = level
|
48
|
+
|
49
|
+
[klass.to_sym, character_class_properties]
|
50
|
+
end.to_h
|
51
|
+
end
|
52
|
+
|
53
|
+
def name
|
54
|
+
@properties[:name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def max_hp
|
58
|
+
@properties[:max_hp]
|
59
|
+
end
|
60
|
+
|
61
|
+
def armor_class
|
62
|
+
equipped_ac
|
63
|
+
end
|
64
|
+
|
65
|
+
def level
|
66
|
+
@properties[:level]
|
67
|
+
end
|
68
|
+
|
69
|
+
def size
|
70
|
+
@properties[:size] || @race_properties[:size]
|
71
|
+
end
|
72
|
+
|
73
|
+
def token
|
74
|
+
@properties[:token]
|
75
|
+
end
|
76
|
+
|
77
|
+
def speed
|
78
|
+
if subrace
|
79
|
+
return (@race_properties.dig(:subrace, subrace.to_sym,
|
80
|
+
:base_speed) || @race_properties[:base_speed])
|
81
|
+
end
|
82
|
+
@race_properties[:base_speed]
|
83
|
+
end
|
84
|
+
|
85
|
+
def race
|
86
|
+
@properties[:race]
|
87
|
+
end
|
88
|
+
|
89
|
+
def subrace
|
90
|
+
@properties[:subrace]
|
91
|
+
end
|
92
|
+
|
93
|
+
def languages
|
94
|
+
class_languages = []
|
95
|
+
@class_properties.values.each do |prop|
|
96
|
+
class_languages += prop[:languages] || []
|
97
|
+
end
|
98
|
+
|
99
|
+
racial_languages = @race_properties[:languages] || []
|
100
|
+
|
101
|
+
(super + class_languages + racial_languages).sort
|
102
|
+
end
|
103
|
+
|
104
|
+
def c_class
|
105
|
+
@properties[:classes]
|
106
|
+
end
|
107
|
+
|
108
|
+
def passive_perception
|
109
|
+
10 + wis_mod + wisdom_proficiency
|
110
|
+
end
|
111
|
+
|
112
|
+
def passive_investigation
|
113
|
+
10 + int_mod + investigation_proficiency
|
114
|
+
end
|
115
|
+
|
116
|
+
def passive_insight
|
117
|
+
10 + wis_mod + insight_proficiency
|
118
|
+
end
|
119
|
+
|
120
|
+
def wisdom_proficiency
|
121
|
+
perception_proficient? ? proficiency_bonus : 0
|
122
|
+
end
|
123
|
+
|
124
|
+
def investigation_proficiency
|
125
|
+
investigation_proficient? ? proficiency_bonus : 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def insight_proficiency
|
129
|
+
insight_proficient? ? proficiency_bonus : 0
|
130
|
+
end
|
131
|
+
|
132
|
+
def proficiency_bonus
|
133
|
+
proficiency_bonus_table[level - 1]
|
134
|
+
end
|
135
|
+
|
136
|
+
def proficient?(prof)
|
137
|
+
return true if @class_properties.values.detect { |c| c[:proficiencies]&.include?(prof) }
|
138
|
+
return true if @race_properties[:skills]&.include?(prof)
|
139
|
+
return true if weapon_proficiencies.include?(prof)
|
140
|
+
|
141
|
+
super
|
142
|
+
end
|
143
|
+
|
144
|
+
def proficient_with_weapon?(weapon)
|
145
|
+
weapon = @session.load_thing weapon if weapon.is_a?(String)
|
146
|
+
|
147
|
+
all_weapon_proficiencies = weapon_proficiencies
|
148
|
+
|
149
|
+
return true if all_weapon_proficiencies.include?(weapon[:name])
|
150
|
+
|
151
|
+
all_weapon_proficiencies&.detect do |prof|
|
152
|
+
weapon[:proficiency_type]&.include?(prof)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def weapon_proficiencies
|
157
|
+
all_weapon_proficiencies = @class_properties.values.map do |p|
|
158
|
+
p[:weapon_proficiencies]
|
159
|
+
end.compact.flatten + @properties.fetch(:weapon_proficiencies, [])
|
160
|
+
|
161
|
+
all_weapon_proficiencies += @race_properties.fetch(:weapon_proficiencies, [])
|
162
|
+
if subrace
|
163
|
+
all_weapon_proficiencies += (@race_properties.dig(:subrace, subrace.to_sym,
|
164
|
+
:weapon_proficiencies) || [])
|
165
|
+
end
|
166
|
+
|
167
|
+
all_weapon_proficiencies
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_h
|
171
|
+
{
|
172
|
+
name: name,
|
173
|
+
classes: c_class,
|
174
|
+
hp: hp,
|
175
|
+
ability: {
|
176
|
+
str: @ability_scores.fetch(:str),
|
177
|
+
dex: @ability_scores.fetch(:dex),
|
178
|
+
con: @ability_scores.fetch(:con),
|
179
|
+
int: @ability_scores.fetch(:int),
|
180
|
+
wis: @ability_scores.fetch(:wis),
|
181
|
+
cha: @ability_scores.fetch(:cha)
|
182
|
+
},
|
183
|
+
passive: {
|
184
|
+
perception: passive_perception,
|
185
|
+
investigation: passive_investigation,
|
186
|
+
insight: passive_insight
|
187
|
+
}
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
def melee_distance
|
192
|
+
(@properties[:equipped].map do |item|
|
193
|
+
weapon_detail = session.load_weapon(item)
|
194
|
+
next if weapon_detail.nil?
|
195
|
+
next unless weapon_detail[:type] == 'melee_attack'
|
196
|
+
|
197
|
+
weapon_detail[:range]
|
198
|
+
end.compact + [5]).max
|
199
|
+
end
|
200
|
+
|
201
|
+
def darkvision?(distance)
|
202
|
+
return true if super
|
203
|
+
|
204
|
+
!!(@race_properties[:darkvision] && @race_properties[:darkvision] >= distance)
|
205
|
+
end
|
206
|
+
|
207
|
+
def player_character_attack_actions(_battle, opportunity_attack: false)
|
208
|
+
# check all equipped and create attack for each
|
209
|
+
valid_weapon_types = if opportunity_attack
|
210
|
+
%w[melee_attack]
|
211
|
+
else
|
212
|
+
%w[ranged_attack melee_attack]
|
213
|
+
end
|
214
|
+
|
215
|
+
weapon_attacks = @properties[:equipped].map do |item|
|
216
|
+
weapon_detail = session.load_weapon(item)
|
217
|
+
next if weapon_detail.nil?
|
218
|
+
next unless valid_weapon_types.include?(weapon_detail[:type])
|
219
|
+
next if weapon_detail[:ammo] && !item_count(weapon_detail[:ammo]).positive?
|
220
|
+
|
221
|
+
attacks = []
|
222
|
+
|
223
|
+
action = AttackAction.new(session, self, :attack)
|
224
|
+
action.using = item
|
225
|
+
attacks << action
|
226
|
+
|
227
|
+
if !opportunity_attack && weapon_detail[:properties] && weapon_detail[:properties].include?('thrown')
|
228
|
+
action = AttackAction.new(session, self, :attack)
|
229
|
+
action.using = item
|
230
|
+
action.thrown = true
|
231
|
+
attacks << action
|
232
|
+
end
|
233
|
+
|
234
|
+
attacks
|
235
|
+
end.flatten.compact
|
236
|
+
|
237
|
+
unarmed_attack = AttackAction.new(session, self, :attack)
|
238
|
+
unarmed_attack.using = 'unarmed_attack'
|
239
|
+
|
240
|
+
weapon_attacks + [unarmed_attack]
|
241
|
+
end
|
242
|
+
|
243
|
+
def available_actions(session, battle, opportunity_attack: false)
|
244
|
+
return [] if unconscious?
|
245
|
+
|
246
|
+
if opportunity_attack && AttackAction.can?(self, battle, opportunity_attack: true)
|
247
|
+
return player_character_attack_actions(battle, opportunity_attack: true)
|
248
|
+
end
|
249
|
+
|
250
|
+
ACTION_LIST.map do |type|
|
251
|
+
next unless "#{type.to_s.camelize}Action".constantize.can?(self, battle)
|
252
|
+
|
253
|
+
case type
|
254
|
+
when :look
|
255
|
+
LookAction.new(session, self, :look)
|
256
|
+
when :attack
|
257
|
+
player_character_attack_actions(battle)
|
258
|
+
when :dodge
|
259
|
+
DodgeAction.new(session, self, :dodge)
|
260
|
+
when :help
|
261
|
+
action = HelpAction.new(session, self, :help)
|
262
|
+
action
|
263
|
+
when :hide
|
264
|
+
HideAction.new(session, self, :hide)
|
265
|
+
when :hide_bonus
|
266
|
+
action = HideBonusAction.new(session, self, :hide_bonus)
|
267
|
+
action.as_bonus_action = true
|
268
|
+
action
|
269
|
+
when :disengage_bonus
|
270
|
+
action = DisengageAction.new(session, self, :disengage_bonus)
|
271
|
+
action.as_bonus_action = true
|
272
|
+
action
|
273
|
+
when :disengage
|
274
|
+
DisengageAction.new(session, self, :disengage)
|
275
|
+
when :drop_grapple
|
276
|
+
DropGrappleAction.new(session, self, :drop_grapple)
|
277
|
+
when :grapple
|
278
|
+
GrappleAction.new(session, self, :grapple)
|
279
|
+
when :escape_grapple
|
280
|
+
EscapeGrappleAction.new(session, self, :escape_grapple)
|
281
|
+
when :move
|
282
|
+
MoveAction.new(session, self, type)
|
283
|
+
when :prone
|
284
|
+
ProneAction.new(session, self, type)
|
285
|
+
when :stand
|
286
|
+
StandAction.new(session, self, type)
|
287
|
+
when :short_rest
|
288
|
+
ShortRestAction.new(session, self, type)
|
289
|
+
when :dash_bonus
|
290
|
+
action = DashBonusAction.new(session, self, :dash_bonus)
|
291
|
+
action.as_bonus_action = true
|
292
|
+
action
|
293
|
+
when :dash
|
294
|
+
action = DashAction.new(session, self, type)
|
295
|
+
action
|
296
|
+
when :use_item
|
297
|
+
UseItemAction.new(session, self, type)
|
298
|
+
when :interact
|
299
|
+
InteractAction.new(session, self, type)
|
300
|
+
when :ground_interact
|
301
|
+
GroundInteractAction.new(session, self, type)
|
302
|
+
when :inventory
|
303
|
+
InventoryAction.new(session, self, type)
|
304
|
+
when :first_aid
|
305
|
+
FirstAidAction.new(session, self, type)
|
306
|
+
when :shove
|
307
|
+
action = ShoveAction.new(session, self, type)
|
308
|
+
action.knock_prone = true
|
309
|
+
action
|
310
|
+
when :two_weapon_attack
|
311
|
+
two_weapon_attack_actions(battle)
|
312
|
+
when :push
|
313
|
+
ShoveAction.new(session, self, type)
|
314
|
+
else
|
315
|
+
Natural20::Action.new(session, self, type)
|
316
|
+
end
|
317
|
+
end.compact.flatten + c_class.keys.map { |c| send(:"special_actions_for_#{c}", session, battle) }.flatten
|
318
|
+
end
|
319
|
+
|
320
|
+
def two_weapon_attack_actions(battle)
|
321
|
+
@properties[:equipped].each do |item|
|
322
|
+
weapon_detail = session.load_weapon(item)
|
323
|
+
next if weapon_detail.nil?
|
324
|
+
next unless weapon_detail[:type] == 'melee_attack'
|
325
|
+
|
326
|
+
next unless weapon_detail[:properties] && weapon_detail[:properties].include?('light') && TwoWeaponAttackAction.can?(
|
327
|
+
self, battle, weapon: weapon_detail[:name]
|
328
|
+
)
|
329
|
+
|
330
|
+
action = TwoWeaponAttackAction.new(session, self, :attack)
|
331
|
+
action.using = item
|
332
|
+
return action
|
333
|
+
end
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
def available_interactions(_entity, _battle)
|
338
|
+
[]
|
339
|
+
end
|
340
|
+
|
341
|
+
def class_feature?(feature)
|
342
|
+
return true if @properties[:class_features]&.include?(feature)
|
343
|
+
return true if @properties[:attributes]&.include?(feature)
|
344
|
+
return true if @race_properties[:race_features]&.include?(feature)
|
345
|
+
return true if subrace && @race_properties.dig(:subrace, subrace.to_sym, :class_features)&.include?(feature)
|
346
|
+
return true if subrace && @race_properties.dig(:subrace, subrace.to_sym, :race_features)&.include?(feature)
|
347
|
+
|
348
|
+
@class_properties.values.detect { |p| p[:class_features]&.include?(feature) }
|
349
|
+
end
|
350
|
+
|
351
|
+
# Loads a pregen character from path
|
352
|
+
# @param session [Natural20::Session] The session to use
|
353
|
+
# @param path [String] path to character sheet YAML
|
354
|
+
# @apram override [Hash] override attributes
|
355
|
+
# @return [Natural20::PlayerCharacter] An instance of PlayerCharacter
|
356
|
+
def self.load(session, path, override = {})
|
357
|
+
Natural20::PlayerCharacter.new(session, YAML.load_file(path).deep_symbolize_keys!.merge(override))
|
358
|
+
end
|
359
|
+
|
360
|
+
# returns if an npc or a player character
|
361
|
+
# @return [Boolean]
|
362
|
+
def npc?
|
363
|
+
false
|
364
|
+
end
|
365
|
+
|
366
|
+
def pc?
|
367
|
+
true
|
368
|
+
end
|
369
|
+
|
370
|
+
# @param hit_die_num [Integer] number of hit die to use
|
371
|
+
def short_rest!(battle, prompt: false)
|
372
|
+
super
|
373
|
+
|
374
|
+
@class_properties.keys do |klass|
|
375
|
+
send(:"short_rest_for_#{klass}") if respond_to?(:"short_rest_for_#{klass}")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
def proficiency_bonus_table
|
382
|
+
[2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6]
|
383
|
+
end
|
384
|
+
|
385
|
+
def setup_attributes
|
386
|
+
super
|
387
|
+
@hp = @properties[:max_hp]
|
388
|
+
end
|
389
|
+
|
390
|
+
def equipped_ac
|
391
|
+
@equipments ||= YAML.load_file(File.join(session.root_path, 'items', 'equipment.yml')).deep_symbolize_keys!
|
392
|
+
|
393
|
+
equipped_meta = @equipped.map { |e| @equipments[e.to_sym] }.compact
|
394
|
+
armor = equipped_meta.detect do |equipment|
|
395
|
+
equipment[:type] == 'armor'
|
396
|
+
end
|
397
|
+
|
398
|
+
shield = equipped_meta.detect { |e| e[:type] == 'shield' }
|
399
|
+
|
400
|
+
armor_ac = if armor.nil?
|
401
|
+
10 + dex_mod
|
402
|
+
else
|
403
|
+
armor[:ac] + (if armor[:mod_cap]
|
404
|
+
[dex_mod,
|
405
|
+
armor[:mod_cap]].min
|
406
|
+
else
|
407
|
+
dex_mod
|
408
|
+
end) + (class_feature?('defense') ? 1 : 0)
|
409
|
+
end
|
410
|
+
|
411
|
+
armor_ac + (shield.nil? ? 0 : shield[:bonus_ac])
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|