natural_20 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +22 -0
- data/fixtures/hide_test.yml +27 -0
- data/items/objects.yml +23 -0
- data/items/weapons.yml +57 -0
- data/lib/natural_20/actions/attack_action.rb +10 -8
- data/lib/natural_20/actions/first_aid_action.rb +1 -0
- data/lib/natural_20/actions/ground_interact_action.rb +5 -0
- data/lib/natural_20/actions/hide_action.rb +4 -3
- data/lib/natural_20/actions/move_action.rb +3 -26
- data/lib/natural_20/ai_controller/standard.rb +15 -2
- data/lib/natural_20/battle.rb +12 -2
- data/lib/natural_20/battle_map.rb +17 -3
- data/lib/natural_20/cli/character_builder.rb +11 -2
- data/lib/natural_20/cli/commandline_ui.rb +6 -5
- data/lib/natural_20/cli/map_renderer.rb +15 -3
- data/lib/natural_20/concerns/entity.rb +3 -5
- data/lib/natural_20/concerns/movement_helper.rb +33 -1
- data/lib/natural_20/concerns/navigation.rb +2 -1
- data/lib/natural_20/item_library/object.rb +15 -0
- data/lib/natural_20/player_character.rb +17 -7
- data/lib/natural_20/utils/cover.rb +7 -0
- data/lib/natural_20/version.rb +1 -1
- data/locales/en.yml +12 -2
- data/natural_20-0.1.0.gem +0 -0
- data/races/dwarf.yml +35 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e33ce92d3b1d8ebf75e0262bb56ab3e990702897632c2ebdb476e814d8abfe4a
|
4
|
+
data.tar.gz: c3aa5ca94f3d587466f4ec93fd44c05a58af2f009cd58cdf50f36b9e9be1bd75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 568d09d82b1bdfb16696f19082e673ce540f01ff9688ec8293898685497df8c14c4b8f29ccb42ec6b2d7c0e971645074f05a282dd76e347acf0e45b7744fb95f
|
7
|
+
data.tar.gz: a2896d2bc69e40345675a7b4243b18b72562266ee39c32017ea420b00b68d996f12e0fbe410fc0fa10383dfdcc205a0be7c7d70f044605460281275f8b1b59dc
|
data/README.md
CHANGED
@@ -9,12 +9,14 @@ Features:
|
|
9
9
|
- Simulation of doors, traps, treasure chests and cover
|
10
10
|
- Rudimentary AI and pathfinding
|
11
11
|
- Text based UI
|
12
|
+
- Support for automatic and manual dice rolling
|
12
13
|
- Easily extensible to incorporate in your own games
|
13
14
|
|
14
15
|
Supported Races:
|
15
16
|
- Human
|
16
17
|
- Elf
|
17
18
|
- Halfling
|
19
|
+
- Dwarf
|
18
20
|
- More to come
|
19
21
|
|
20
22
|
Supported Classes;
|
@@ -69,6 +71,13 @@ game.yml
|
|
69
71
|
|
70
72
|
These are all text readable for you to customize to your liking.
|
71
73
|
|
74
|
+
## Screenshots
|
75
|
+
|
76
|
+

|
77
|
+

|
78
|
+

|
79
|
+
|
80
|
+
|
72
81
|
## Creating your own adventures
|
73
82
|
|
74
83
|
You can generate a skeleton adventure using:
|
@@ -79,6 +88,19 @@ nat20author
|
|
79
88
|
|
80
89
|
A prompt based system will launch for you to create your own game.
|
81
90
|
|
91
|
+
The end result will be a folder containing a bunch of YAML files describing your game
|
92
|
+
|
93
|
+
## Map making guide
|
94
|
+
|
95
|
+
A map is a YAML file that consists of the following:
|
96
|
+
|
97
|
+
- Base Layer - contains terrain information like ground, grass, walls, doors, traps and various obstacles
|
98
|
+
- Meta Layer - contains information about dynamic objects like player tokens, NPCs
|
99
|
+
- Light Layer - contains information about static lights in the map
|
100
|
+
- Legend - A mapping from the layers to details about the terrain and tokens
|
101
|
+
- Map Triggers - A map level trigger
|
102
|
+
|
103
|
+
|
82
104
|
## Development
|
83
105
|
|
84
106
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
name: Natural20::Battle Sim
|
2
|
+
description: Map for Natural20::Battle Sim
|
3
|
+
map:
|
4
|
+
base:
|
5
|
+
- ......
|
6
|
+
- ......
|
7
|
+
- .MMMM.
|
8
|
+
- .MMMM.
|
9
|
+
- .MMMM.
|
10
|
+
- ......
|
11
|
+
meta:
|
12
|
+
- ......
|
13
|
+
- ......
|
14
|
+
- ......
|
15
|
+
- ..A...
|
16
|
+
- ......
|
17
|
+
- .B....
|
18
|
+
legend:
|
19
|
+
A:
|
20
|
+
name: spawn_point_1
|
21
|
+
type: spawn_point
|
22
|
+
B:
|
23
|
+
name: spawn_point_2
|
24
|
+
type: spawn_point
|
25
|
+
M:
|
26
|
+
name: Tall Grass
|
27
|
+
type: briar
|
data/items/objects.yml
CHANGED
@@ -61,6 +61,29 @@ water:
|
|
61
61
|
passable: true
|
62
62
|
token:
|
63
63
|
- ^
|
64
|
+
briar:
|
65
|
+
color: green
|
66
|
+
passable: true
|
67
|
+
placeable: true
|
68
|
+
movement_cost: 2
|
69
|
+
cover: half
|
70
|
+
default_ac: 10
|
71
|
+
hp_die: 1d4
|
72
|
+
max_hp: 4
|
73
|
+
allow_hide: true
|
74
|
+
token:
|
75
|
+
- ▓
|
76
|
+
tree:
|
77
|
+
color: green
|
78
|
+
passable: true
|
79
|
+
placeable: true
|
80
|
+
cover: half
|
81
|
+
default_ac: 10
|
82
|
+
hp_die: 1d8
|
83
|
+
max_hp: 8
|
84
|
+
allow_hide: true
|
85
|
+
token:
|
86
|
+
- '*'
|
64
87
|
wooden_door:
|
65
88
|
color: magenta
|
66
89
|
default_ac: 15
|
data/items/weapons.yml
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
---
|
2
|
+
battleaxe:
|
3
|
+
cost: 10
|
4
|
+
damage: 1d8
|
5
|
+
damage_2: 1d10
|
6
|
+
damage_type: slashing
|
7
|
+
meta:
|
8
|
+
noise_source: 5
|
9
|
+
noise_target: 5
|
10
|
+
modifiers: null
|
11
|
+
name: Battleaxe
|
12
|
+
proficiency_type:
|
13
|
+
- martial
|
14
|
+
properties:
|
15
|
+
- versatile
|
16
|
+
range: 5
|
17
|
+
subtype: weapon
|
18
|
+
type: melee_attack
|
19
|
+
weight: 4
|
2
20
|
dagger:
|
3
21
|
cost: 2
|
4
22
|
damage: 1d4
|
@@ -123,6 +141,45 @@ light_crossbow:
|
|
123
141
|
subtype: weapon
|
124
142
|
type: ranged_attack
|
125
143
|
weight: 2
|
144
|
+
light_hammer:
|
145
|
+
cost: 2
|
146
|
+
damage: 1d4
|
147
|
+
damage_type: bludgeoning
|
148
|
+
meta:
|
149
|
+
noise_source: 5
|
150
|
+
noise_target: 5
|
151
|
+
modifiers: null
|
152
|
+
name: Light Hammer
|
153
|
+
proficiency_type:
|
154
|
+
- simple
|
155
|
+
properties:
|
156
|
+
- light
|
157
|
+
- thrown
|
158
|
+
range: 5
|
159
|
+
subtype: weapon
|
160
|
+
thrown:
|
161
|
+
range: 20
|
162
|
+
range_max: 60
|
163
|
+
type: melee_attack
|
164
|
+
weight: 2
|
165
|
+
warhammer:
|
166
|
+
cost: 2
|
167
|
+
damage: 1d8
|
168
|
+
damage_2: 1d10
|
169
|
+
damage_type: bludgeoning
|
170
|
+
meta:
|
171
|
+
noise_source: 5
|
172
|
+
noise_target: 5
|
173
|
+
modifiers: null
|
174
|
+
name: Warhammer
|
175
|
+
proficiency_type:
|
176
|
+
- martial
|
177
|
+
properties:
|
178
|
+
- versatile
|
179
|
+
range: 5
|
180
|
+
subtype: weapon
|
181
|
+
type: melee_attack
|
182
|
+
weight: 2
|
126
183
|
longbow:
|
127
184
|
ammo: arrows
|
128
185
|
cost: 50
|
@@ -11,7 +11,9 @@ class AttackAction < Natural20::Action
|
|
11
11
|
# @param battle [Natural20::Battle]
|
12
12
|
# @return [Boolean]
|
13
13
|
def self.can?(entity, battle, options = {})
|
14
|
-
|
14
|
+
return entity.total_reactions(battle).positive? if battle && options[:opportunity_attack]
|
15
|
+
|
16
|
+
battle.nil? || entity.total_actions(battle).positive? || entity.multiattack?(
|
15
17
|
battle, options[:npc_action]
|
16
18
|
)
|
17
19
|
end
|
@@ -109,11 +111,11 @@ class AttackAction < Natural20::Action
|
|
109
111
|
end
|
110
112
|
|
111
113
|
if as_reaction
|
112
|
-
battle.
|
114
|
+
battle.consume(item[:source], :reaction)
|
113
115
|
elsif item[:second_hand]
|
114
|
-
battle.
|
116
|
+
battle.consume(item[:source], :bonus_action)
|
115
117
|
else
|
116
|
-
battle.
|
118
|
+
battle.consume(item[:source], :action)
|
117
119
|
end
|
118
120
|
|
119
121
|
item[:source].break_stealth!(battle)
|
@@ -339,7 +341,7 @@ class AttackAction < Natural20::Action
|
|
339
341
|
_advantage, disadvantage = adv_info
|
340
342
|
disadvantage << :protection
|
341
343
|
@advantage_mod = -1
|
342
|
-
battle.
|
344
|
+
battle.consume(entity, :reaction)
|
343
345
|
end
|
344
346
|
end
|
345
347
|
|
@@ -357,9 +359,9 @@ class TwoWeaponAttackAction < AttackAction
|
|
357
359
|
# @param entity [Natural20::Entity]
|
358
360
|
# @param battle [Natural20::Battle]
|
359
361
|
def self.can?(entity, battle, options = {})
|
360
|
-
battle.nil? || (entity.total_bonus_actions(battle).positive? && battle.two_weapon_attack?(entity) && options[:weapon] != battle.first_hand_weapon(entity) || entity.equipped_weapons.select do |a|
|
361
|
-
a == battle.first_hand_weapon(entity)
|
362
|
-
end.size >= 2)
|
362
|
+
battle.nil? || (entity.total_bonus_actions(battle).positive? && battle.two_weapon_attack?(entity) && (options[:weapon] != battle.first_hand_weapon(entity) || entity.equipped_weapons.select do |a|
|
363
|
+
a.to_s == battle.first_hand_weapon(entity)
|
364
|
+
end.size >= 2))
|
363
365
|
end
|
364
366
|
|
365
367
|
def second_hand
|
@@ -9,6 +9,7 @@ class FirstAidAction < Natural20::Action
|
|
9
9
|
# @param battle [Natural20::Battle]
|
10
10
|
def self.unconscious_targets(entity, battle)
|
11
11
|
return [] unless battle
|
12
|
+
return [] unless battle.map
|
12
13
|
|
13
14
|
adjacent_squares = entity.melee_squares(battle.map, adjacent_only: true)
|
14
15
|
entities = []
|
@@ -11,7 +11,12 @@ class GroundInteractAction < Natural20::Action
|
|
11
11
|
action.build_map
|
12
12
|
end
|
13
13
|
|
14
|
+
# @param entity [Natural20::Entity]
|
15
|
+
# @param battle [Natural20::Battle]
|
16
|
+
# @return [Integer]
|
14
17
|
def self.items_on_the_ground_count(entity, battle)
|
18
|
+
return 0 unless battle.map
|
19
|
+
|
15
20
|
battle.map.items_on_the_ground(entity).inject(0) { |total, item| total + item[1].size }
|
16
21
|
end
|
17
22
|
|
@@ -21,6 +21,7 @@ class HideAction < Natural20::Action
|
|
21
21
|
stealth_roll = @source.stealth_check!(opts[:battle])
|
22
22
|
@result = [{
|
23
23
|
source: @source,
|
24
|
+
bonus_action: as_bonus_action,
|
24
25
|
type: :hide,
|
25
26
|
roll: stealth_roll,
|
26
27
|
battle: opts[:battle]
|
@@ -37,10 +38,10 @@ class HideAction < Natural20::Action
|
|
37
38
|
item[:source].hiding!(battle, item[:roll].result)
|
38
39
|
end
|
39
40
|
|
40
|
-
if
|
41
|
-
battle.
|
41
|
+
if item[:bonus_action]
|
42
|
+
battle.consume(item[:source], :bonus_action)
|
42
43
|
else
|
43
|
-
battle.
|
44
|
+
battle.consume(item[:source], :action)
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
@@ -127,42 +127,19 @@ class MoveAction < Natural20::Action
|
|
127
127
|
# @param move_list [Array<Array<Integer,Integer>>]
|
128
128
|
# @param battle [Natural20::Battle]
|
129
129
|
def check_opportunity_attacks(entity, move_list, battle, grappled: false)
|
130
|
-
if 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])
|
130
|
+
if battle
|
135
131
|
|
132
|
+
retrieve_opportunity_attacks(entity, move_list, battle).each do |enemy_opporunity|
|
136
133
|
original_location = move_list[0...enemy_opporunity[:path]]
|
137
134
|
attack_location = original_location.last
|
138
135
|
battle.trigger_opportunity_attack(enemy_opporunity[:source], entity, *attack_location)
|
139
136
|
|
140
|
-
if !grappled && !entity.conscious?
|
141
|
-
move_list = original_location
|
142
|
-
break
|
143
|
-
end
|
137
|
+
return original_location if !grappled && !entity.conscious?
|
144
138
|
end
|
145
139
|
end
|
146
140
|
move_list
|
147
141
|
end
|
148
142
|
|
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
143
|
def apply!(battle)
|
167
144
|
@result.each do |item|
|
168
145
|
case (item[:type])
|
@@ -127,7 +127,7 @@ module AiController
|
|
127
127
|
# generate available targets
|
128
128
|
valid_actions = []
|
129
129
|
|
130
|
-
if enemy_positions.empty? && LookAction.can?(entity, battle)
|
130
|
+
if enemy_positions.empty? && investigate_location.empty? && LookAction.can?(entity, battle)
|
131
131
|
action = LookAction.new(battle.session, entity, :look)
|
132
132
|
return action
|
133
133
|
end
|
@@ -163,6 +163,19 @@ module AiController
|
|
163
163
|
valid_actions += generate_moves_for_positions(battle, entity, investigate_location)
|
164
164
|
end
|
165
165
|
|
166
|
+
if HideBonusAction.can?(entity, battle) # bonus action hide if able
|
167
|
+
hide_action = HideBonusAction.new(battle.session, entity, :hide_bonus)
|
168
|
+
hide_action.as_bonus_action = true
|
169
|
+
valid_actions << hide_action
|
170
|
+
end
|
171
|
+
|
172
|
+
if valid_actions.first&.action_type == :move && DisengageBonusAction.can?(entity,
|
173
|
+
battle) && !retrieve_opportunity_attacks(
|
174
|
+
entity, valid_actions.first.move_path, battle
|
175
|
+
).empty?
|
176
|
+
return DisengageBonusAction.new(battle.session, entity, :disengage_bonus)
|
177
|
+
end
|
178
|
+
|
166
179
|
valid_actions << DodgeAction.new(battle.session, entity, :dodge) if entity.action?(battle)
|
167
180
|
|
168
181
|
return valid_actions.first unless valid_actions.empty?
|
@@ -195,7 +208,7 @@ module AiController
|
|
195
208
|
melee_weight = 2.0
|
196
209
|
end
|
197
210
|
|
198
|
-
defense_weight = 2.0 if (entity.hp / entity.max_hp) < 0.25
|
211
|
+
# defense_weight = 2.0 if (entity.hp / entity.max_hp) < 0.25
|
199
212
|
|
200
213
|
[square, melee_weight * melee + range_weight * ranged + defense_weight * defense + mobility * mobolity_weight]
|
201
214
|
end
|
data/lib/natural_20/battle.rb
CHANGED
@@ -64,7 +64,7 @@ module Natural20
|
|
64
64
|
|
65
65
|
# Adds an entity to the battle
|
66
66
|
# @param entity [Natural20::Entity] The entity to add to the battle
|
67
|
-
# @param group [Symbol] A symbol denoting which group this entity belongs to
|
67
|
+
# @param group [Symbol] A symbol denoting which opposing group this entity belongs to
|
68
68
|
# @param controller [AiController::Standard] Ai controller to use
|
69
69
|
# @param position [Array, Symbol] Starting location in the map can be a position or a spawn point
|
70
70
|
# @param token [String, Symbol] The token to use
|
@@ -279,6 +279,7 @@ module Natural20
|
|
279
279
|
|
280
280
|
cover_value = @map.cover_calculation(@map, entity1, entity2, entity_1_pos: entity_1_pos,
|
281
281
|
naturally_stealthy: entity2.class_feature?('naturally_stealthy'))
|
282
|
+
|
282
283
|
if cover_value.positive?
|
283
284
|
entity_2_state = entity_state_for(entity2)
|
284
285
|
return false if entity_2_state[:stealth] > [active_perception, entity1.passive_perception].max
|
@@ -382,7 +383,7 @@ module Natural20
|
|
382
383
|
|
383
384
|
def while_active(max_rounds = nil, &block)
|
384
385
|
loop do
|
385
|
-
Natural20::EventManager.received_event(source: self, event: :start_of_round)
|
386
|
+
Natural20::EventManager.received_event(source: self, event: :start_of_round, in_battle: ongoing?)
|
386
387
|
|
387
388
|
current_turn.death_saving_throw!(self) if current_turn.unconscious? && !current_turn.stable?
|
388
389
|
|
@@ -522,6 +523,15 @@ module Natural20
|
|
522
523
|
@map.activate_map_triggers(event, source, opt.merge(ui_controller: controller_for(source)))
|
523
524
|
end
|
524
525
|
|
526
|
+
# Consumes an action resource
|
527
|
+
# @param entity [Natural20::Entity]
|
528
|
+
# @param resouce [Symbol]
|
529
|
+
def consume(entity, resource, qty = 1)
|
530
|
+
raise 'invalid resource' unless %i[action reaction bonus_action movement].include?(resource.to_sym)
|
531
|
+
|
532
|
+
entity_state_for(entity)[resource.to_sym] = [0, entity_state_for(entity)[resource.to_sym] - qty].max
|
533
|
+
end
|
534
|
+
|
525
535
|
protected
|
526
536
|
|
527
537
|
def t(key, options = {})
|
@@ -135,7 +135,7 @@ module Natural20
|
|
135
135
|
# Get object at map location
|
136
136
|
# @param pos_x [Integer]
|
137
137
|
# @param pos_y [Integer]
|
138
|
-
# @return [ItemLibrary::Object]
|
138
|
+
# @return [Array<ItemLibrary::Object>]
|
139
139
|
def objects_at(pos_x, pos_y)
|
140
140
|
@objects[pos_x][pos_y]
|
141
141
|
end
|
@@ -228,13 +228,13 @@ module Natural20
|
|
228
228
|
# @param entity1 [Natural20::Entity]
|
229
229
|
# @param entity2 [Natural20::Entity]
|
230
230
|
# @result [Integer]
|
231
|
-
def distance(entity1, entity2, entity_1_pos: nil)
|
231
|
+
def distance(entity1, entity2, entity_1_pos: nil, entity_2_pos: nil)
|
232
232
|
raise 'entity 1 param cannot be nil' if entity1.nil?
|
233
233
|
raise 'entity 2 param cannot be nil' if entity2.nil?
|
234
234
|
|
235
235
|
# entity 1 squares
|
236
236
|
entity_1_sq = entity_1_pos ? entity_squares_at_pos(entity1, *entity_1_pos) : entity_squares(entity1)
|
237
|
-
entity_2_sq = entity_squares(entity2)
|
237
|
+
entity_2_sq = entity_2_pos ? entity_squares_at_pos(entity2, *entity_2_pos) : entity_squares(entity2)
|
238
238
|
|
239
239
|
entity_1_sq.map do |ent1_pos|
|
240
240
|
entity_2_sq.map do |ent2_pos|
|
@@ -245,6 +245,20 @@ module Natural20
|
|
245
245
|
end.flatten.min
|
246
246
|
end
|
247
247
|
|
248
|
+
# Returns the line distance between an entity and a location
|
249
|
+
# @param entity [Natural20::Entity]
|
250
|
+
# @param pos2_x [Integer]
|
251
|
+
# @param pos2_y [Integer]
|
252
|
+
# @result [Integer]
|
253
|
+
def line_distance(entity1, pos2_x, pos2_y, entity_1_pos: nil)
|
254
|
+
entity_1_sq = entity_1_pos ? entity_squares_at_pos(entity1, *entity_1_pos) : entity_squares(entity1)
|
255
|
+
|
256
|
+
entity_1_sq.map do |ent1_pos|
|
257
|
+
pos1_x, pos1_y = ent1_pos
|
258
|
+
Math.sqrt((pos1_x - pos2_x)**2 + (pos1_y - pos2_y)**2).floor
|
259
|
+
end.flatten.min
|
260
|
+
end
|
261
|
+
|
248
262
|
# Get all the location of the squares occupied by said entity (e.g. for large, huge creatures)
|
249
263
|
# @param entity [Natural20::Entity]
|
250
264
|
# @return [Array]
|
@@ -21,14 +21,14 @@ module Natural20
|
|
21
21
|
ability: {},
|
22
22
|
skills: [],
|
23
23
|
level: 1,
|
24
|
-
token: ['X']
|
24
|
+
token: ['X'],
|
25
|
+
tools: []
|
25
26
|
}
|
26
27
|
loop do
|
27
28
|
ability_method = :random
|
28
29
|
|
29
30
|
@values[:name] = prompt.ask(t('builder.enter_name'), default: @values[:name]) do |q|
|
30
31
|
q.required true
|
31
|
-
q.validate(/\A\w+\Z/)
|
32
32
|
q.modify :capitalize
|
33
33
|
end
|
34
34
|
|
@@ -88,6 +88,15 @@ module Natural20
|
|
88
88
|
@values[:classes][k.to_sym] = 1
|
89
89
|
@class_properties = session.load_class(k)
|
90
90
|
|
91
|
+
if race_detail[:tool_proficiencies_choice]
|
92
|
+
num_tools_choices = race_detail[:tool_proficiencies_choice]
|
93
|
+
@values[:tools] = prompt.multi_select(t('builder.select_tools_proficiency'), min: num_tools_choices, max: num_tools_choices) do |q|
|
94
|
+
race_detail[:tool_proficiencies].each do |prof|
|
95
|
+
q.choice t("builder.tools.#{prof}"), prof
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
91
100
|
ability_method = prompt.select(t('builder.ability_score_method')) do |q|
|
92
101
|
q.choice t('builder.ability_score.random'), :random
|
93
102
|
q.choice t('builder.ability_score.fixed'), :fixed
|
@@ -468,7 +468,7 @@ class CommandlineUI < Natural20::Controller
|
|
468
468
|
def describe_map(map, line_of_sight: [])
|
469
469
|
line_of_sight = [line_of_sight] unless line_of_sight.is_a?(Array)
|
470
470
|
pov = line_of_sight.map(&:name).join(',')
|
471
|
-
puts
|
471
|
+
puts t('map_description', width: map.size[0], length: map.size[1], feet_per_grid: map.feet_per_grid, pov: pov)
|
472
472
|
end
|
473
473
|
|
474
474
|
# Return moves by a player using the commandline UI
|
@@ -485,10 +485,10 @@ class CommandlineUI < Natural20::Controller
|
|
485
485
|
puts t(:character_status_line, hp: entity.hp, max_hp: entity.max_hp, total_actions: entity.total_actions(battle), bonus_action: entity.total_bonus_actions(battle),
|
486
486
|
available_movement: entity.available_movement(battle), statuses: entity.statuses.to_a.join(','))
|
487
487
|
|
488
|
-
action = prompt.select(
|
489
|
-
|
490
|
-
entity.available_actions(@session, battle).each do |
|
491
|
-
menu.choice
|
488
|
+
action = prompt.select(t('character_action_prompt', name: entity.name, token: entity.token&.first), per_page: TTY_PROMPT_PER_PAGE,
|
489
|
+
filter: true) do |menu|
|
490
|
+
entity.available_actions(@session, battle).each do |a|
|
491
|
+
menu.choice a.label, a
|
492
492
|
end
|
493
493
|
# menu.choice 'Console (Developer Mode)', :console
|
494
494
|
menu.choice 'End'.colorize(:red), :end
|
@@ -523,6 +523,7 @@ class CommandlineUI < Natural20::Controller
|
|
523
523
|
cycles += 1
|
524
524
|
session.save_game(battle)
|
525
525
|
action = battle.move_for(entity)
|
526
|
+
|
526
527
|
if action.nil?
|
527
528
|
|
528
529
|
unless battle.current_party.include?(entity)
|
@@ -15,11 +15,23 @@ module Natural20
|
|
15
15
|
# @param select_pos [Array] coordinate position to render selection cursor
|
16
16
|
# @param update_on_drop [Boolean] If true, only render line of sight if movement has been confirmed
|
17
17
|
# @return [String]
|
18
|
-
def render(entity: nil, line_of_sight: nil, path: [], acrobatics_checks: [], athletics_checks: [], select_pos: nil, update_on_drop: true, path_char: nil, highlight: {}
|
18
|
+
def render(entity: nil, line_of_sight: nil, path: [], acrobatics_checks: [], athletics_checks: [], select_pos: nil, update_on_drop: true, path_char: nil, highlight: {}, viewport_size: nil, top_position: [
|
19
|
+
0, 0
|
20
|
+
])
|
19
21
|
highlight_positions = highlight.keys.map { |entity| @map.entity_squares(entity) }.flatten(1)
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
+
viewport_size ||= map.size
|
24
|
+
|
25
|
+
top_x, top_y = top_position
|
26
|
+
top_x = map.size[0] - top_x < viewport_size[0] ? map.size[0] - viewport_size[0] : top_x if viewport_size
|
27
|
+
top_y = map.size[0] - top_y < viewport_size[1] ? map.size[1] - viewport_size[1] : top_y if viewport_size
|
28
|
+
|
29
|
+
right_x = top_x + viewport_size[0] >= map.size[1] ? map.size[1] - 1 : top_x + viewport_size[0]
|
30
|
+
right_y = top_y + viewport_size[1] >= map.size[0] ? map.size[0] - 1 : top_y + viewport_size[1]
|
31
|
+
|
32
|
+
(top_x..right_x).map do |row_index|
|
33
|
+
(top_y..right_y).map do |col_index|
|
34
|
+
c = map.base_map[col_index][row_index]
|
23
35
|
display = render_position(c, col_index, row_index, path: path, override_path_char: path_char, entity: entity, line_of_sight: line_of_sight,
|
24
36
|
update_on_drop: update_on_drop, acrobatics_checks: acrobatics_checks,
|
25
37
|
athletics_checks: athletics_checks)
|
@@ -205,11 +205,7 @@ module Natural20
|
|
205
205
|
ofs_x /= map.feet_per_grid
|
206
206
|
ofs_y /= map.feet_per_grid
|
207
207
|
|
208
|
-
if map.placeable?(self, x + ofs_x, y + ofs_y)
|
209
|
-
[x + ofs_x, y + ofs_y]
|
210
|
-
else
|
211
|
-
nil
|
212
|
-
end
|
208
|
+
[x + ofs_x, y + ofs_y] if map.placeable?(self, x + ofs_x, y + ofs_y)
|
213
209
|
end
|
214
210
|
|
215
211
|
# @param map [Natural20::BattleMap]
|
@@ -591,6 +587,8 @@ module Natural20
|
|
591
587
|
SKILL_AND_ABILITY_MAP.each do |ability, skills|
|
592
588
|
skills.each do |skill|
|
593
589
|
define_method("#{skill}_mod") do
|
590
|
+
return @properties[:skills][skill.to_sym] if npc? && @properties[:skills] && @properties[:skills][skill.to_sym]
|
591
|
+
|
594
592
|
ability_mod = case ability.to_sym
|
595
593
|
when :dex
|
596
594
|
dex_mod
|
@@ -100,7 +100,7 @@ module Natural20::MovementHelper
|
|
100
100
|
original_budget = movement_budget
|
101
101
|
|
102
102
|
current_moves.each_with_index do |m, index|
|
103
|
-
raise
|
103
|
+
raise 'invalid move coordinate' unless m.size == 2 # assert move correctness
|
104
104
|
|
105
105
|
unless index.positive?
|
106
106
|
actual_moves << m
|
@@ -192,4 +192,36 @@ module Natural20::MovementHelper
|
|
192
192
|
movement_budget, impediment)
|
193
193
|
end
|
194
194
|
|
195
|
+
# Checks if a move provokes opportunity attacks
|
196
|
+
# @param entity [Natural20::Entity]
|
197
|
+
# @param move_list [Array<Array<Integer,Integer>>]
|
198
|
+
# @param battle [Natural20::Battle]
|
199
|
+
# @return [Array<Hash>]
|
200
|
+
def retrieve_opportunity_attacks(entity, move_list, battle)
|
201
|
+
return [] if entity.disengage?(battle)
|
202
|
+
|
203
|
+
opportunity_attacks = opportunity_attack_list(entity, move_list, battle, battle.map)
|
204
|
+
opportunity_attacks.select do |enemy_opporunity|
|
205
|
+
enemy_opporunity[:source].has_reaction?(battle) && !entity.grappling_targets.include?(enemy_opporunity[:source])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
|
211
|
+
def opportunity_attack_list(entity, current_moves, battle, map)
|
212
|
+
# get opposing forces
|
213
|
+
opponents = battle.opponents_of?(entity)
|
214
|
+
entered_melee_range = Set.new
|
215
|
+
left_melee_range = []
|
216
|
+
current_moves.each_with_index do |path, index|
|
217
|
+
opponents.each do |enemy|
|
218
|
+
entered_melee_range.add(enemy) if enemy.entered_melee?(map, entity, *path)
|
219
|
+
if !left_melee_range.include?(enemy) && entered_melee_range.include?(enemy) && !enemy.entered_melee?(map,
|
220
|
+
entity, *path)
|
221
|
+
left_melee_range << { source: enemy, path: index }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
left_melee_range
|
226
|
+
end
|
195
227
|
end
|
@@ -59,7 +59,7 @@ module Natural20::Navigation
|
|
59
59
|
support = 0.0
|
60
60
|
|
61
61
|
if melee_attack_squares.key?(d)
|
62
|
-
melee_offence += 0.
|
62
|
+
melee_offence += 0.2
|
63
63
|
defense -= 0.05 * melee_attack_squares[d]
|
64
64
|
if attack_options
|
65
65
|
opponents.each do |opp|
|
@@ -81,6 +81,7 @@ module Natural20::Navigation
|
|
81
81
|
ranged_offence -= 0.5
|
82
82
|
end
|
83
83
|
|
84
|
+
mobility -= 0.001 * map.line_distance(entity, *d)
|
84
85
|
[d, [melee_offence, ranged_offence, defense, mobility, support]]
|
85
86
|
end.to_h
|
86
87
|
end
|
@@ -73,6 +73,17 @@ module ItemLibrary
|
|
73
73
|
@properties[:cover] == 'half'
|
74
74
|
end
|
75
75
|
|
76
|
+
def cover_ac
|
77
|
+
case @properties[:cover].to_sym
|
78
|
+
when :half
|
79
|
+
2
|
80
|
+
when :three_quarter
|
81
|
+
5
|
82
|
+
else
|
83
|
+
0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
76
87
|
def three_quarter_cover?
|
77
88
|
@properties[:cover] == 'three_quarter'
|
78
89
|
end
|
@@ -81,6 +92,10 @@ module ItemLibrary
|
|
81
92
|
@properties[:cover] == 'total'
|
82
93
|
end
|
83
94
|
|
95
|
+
def can_hide?
|
96
|
+
@properties.fetch(:allow_hide, false)
|
97
|
+
end
|
98
|
+
|
84
99
|
def interactable?
|
85
100
|
false
|
86
101
|
end
|
@@ -31,7 +31,7 @@ module Natural20
|
|
31
31
|
@statuses = Set.new
|
32
32
|
@resistances = []
|
33
33
|
entity_uid = SecureRandom.uuid
|
34
|
-
|
34
|
+
|
35
35
|
@max_hit_die = {}
|
36
36
|
@current_hit_die = {}
|
37
37
|
|
@@ -48,6 +48,8 @@ module Natural20
|
|
48
48
|
|
49
49
|
[klass.to_sym, character_class_properties]
|
50
50
|
end.to_h
|
51
|
+
|
52
|
+
setup_attributes
|
51
53
|
end
|
52
54
|
|
53
55
|
def name
|
@@ -55,7 +57,11 @@ module Natural20
|
|
55
57
|
end
|
56
58
|
|
57
59
|
def max_hp
|
58
|
-
|
60
|
+
if class_feature?('dwarven_toughness')
|
61
|
+
@properties[:max_hp] + level
|
62
|
+
else
|
63
|
+
@properties[:max_hp]
|
64
|
+
end
|
59
65
|
end
|
60
66
|
|
61
67
|
def armor_class
|
@@ -243,8 +249,12 @@ module Natural20
|
|
243
249
|
def available_actions(session, battle, opportunity_attack: false)
|
244
250
|
return [] if unconscious?
|
245
251
|
|
246
|
-
if opportunity_attack
|
247
|
-
|
252
|
+
if opportunity_attack
|
253
|
+
if AttackAction.can?(self, battle, opportunity_attack: true)
|
254
|
+
return player_character_attack_actions(battle, opportunity_attack: true)
|
255
|
+
else
|
256
|
+
return []
|
257
|
+
end
|
248
258
|
end
|
249
259
|
|
250
260
|
ACTION_LIST.map do |type|
|
@@ -324,10 +334,10 @@ module Natural20
|
|
324
334
|
next unless weapon_detail[:type] == 'melee_attack'
|
325
335
|
|
326
336
|
next unless weapon_detail[:properties] && weapon_detail[:properties].include?('light') && TwoWeaponAttackAction.can?(
|
327
|
-
self, battle, weapon:
|
337
|
+
self, battle, weapon: item
|
328
338
|
)
|
329
339
|
|
330
|
-
action = TwoWeaponAttackAction.new(session, self, :attack)
|
340
|
+
action = TwoWeaponAttackAction.new(session, self, :attack, weapon: item)
|
331
341
|
action.using = item
|
332
342
|
return action
|
333
343
|
end
|
@@ -384,7 +394,7 @@ module Natural20
|
|
384
394
|
|
385
395
|
def setup_attributes
|
386
396
|
super
|
387
|
-
@hp =
|
397
|
+
@hp = max_hp
|
388
398
|
end
|
389
399
|
|
390
400
|
def equipped_ac
|
@@ -17,6 +17,13 @@ module Natural20::Cover
|
|
17
17
|
next 0 unless cover_characteristics
|
18
18
|
|
19
19
|
max_ac = 0
|
20
|
+
|
21
|
+
# check if any objects in the area provide cover
|
22
|
+
objs = map.objects_at(*target_pos)
|
23
|
+
objs.each do |object|
|
24
|
+
max_ac = [max_ac, object.cover_ac].max if object.can_hide?
|
25
|
+
end
|
26
|
+
|
20
27
|
cover_characteristics.each do |cover|
|
21
28
|
cover_type, pos = cover
|
22
29
|
|
data/lib/natural_20/version.rb
CHANGED
data/locales/en.yml
CHANGED
@@ -42,6 +42,8 @@ en:
|
|
42
42
|
back: Back
|
43
43
|
battle_end: Battle ended in %{num} rounds
|
44
44
|
battle_simulator: Battle Simulator
|
45
|
+
map_description: 'Battle Map (%{width}x%{length}) %{feet_per_grid}ft per square, pov %{pov}:'
|
46
|
+
character_action_prompt: '%{name} (%{token}) will'
|
45
47
|
builder:
|
46
48
|
ability_score:
|
47
49
|
fixed: Fixed ability score 15, 14, 13, 12, 10, 8
|
@@ -70,7 +72,9 @@ en:
|
|
70
72
|
races:
|
71
73
|
dark_elf: Dark Elf
|
72
74
|
high_elf: High Elf
|
75
|
+
hill_dwarf: Hill Dwarf
|
73
76
|
lightfoot: Lightfoot
|
77
|
+
mountain_dwarf: Mountain Dwarf
|
74
78
|
stout: Stout
|
75
79
|
wood_elf: Wood Elf
|
76
80
|
review: Confirm this character?
|
@@ -102,6 +106,10 @@ en:
|
|
102
106
|
str: Select Strength ability score
|
103
107
|
token: Select a token to represent this character on the board
|
104
108
|
token_color: Select a color for your token
|
109
|
+
tools:
|
110
|
+
brewers_supplies: Brewer's Supplies
|
111
|
+
masons_tools: Mason's Tools
|
112
|
+
smiths_tools: Smith's Tools
|
105
113
|
wis: Select Wisdom ability score
|
106
114
|
character_builder: Character Builder ...
|
107
115
|
character_sheet:
|
@@ -118,7 +126,7 @@ en:
|
|
118
126
|
skills: Skills
|
119
127
|
speed: 'Speed: %{speed}ft.'
|
120
128
|
subrace: 'Subrace: %{race}'
|
121
|
-
character_status_line: '%{hp}/%{max_hp} actions: %{total_actions} bonus action: %{bonus_action}, movement: %{available_movement} ft. statuses: %{statuses}'
|
129
|
+
character_status_line: 'HP: %{hp}/%{max_hp} actions: %{total_actions} bonus action: %{bonus_action}, movement: %{available_movement} ft. statuses: %{statuses}'
|
122
130
|
dead_goblin_items: Dead Goblin's items
|
123
131
|
dice_roll:
|
124
132
|
ability_score: 'Rolling for ability score #%{roll_num}'
|
@@ -224,8 +232,8 @@ en:
|
|
224
232
|
thieves_cant: Thieves Cant
|
225
233
|
undercommon: Undercommon
|
226
234
|
manual_target: Target object using the map
|
227
|
-
multiple_target_prompt: Multiple targets at location(s) please select specific targets
|
228
235
|
missing_game: missing game.yml in the current folder
|
236
|
+
multiple_target_prompt: Multiple targets at location(s) please select specific targets
|
229
237
|
'no': 'No'
|
230
238
|
object:
|
231
239
|
Arrows: Arrows
|
@@ -263,6 +271,7 @@ en:
|
|
263
271
|
two_handaxes: Two handaxes
|
264
272
|
two_martial_weapons: (2) Martial Weapons
|
265
273
|
weapons:
|
274
|
+
battleaxe: Battleaxe
|
266
275
|
chain_mail: Chain Mail
|
267
276
|
dagger: Dagger
|
268
277
|
hand_crossbow: Hand Crossbow
|
@@ -273,6 +282,7 @@ en:
|
|
273
282
|
scimitar: Scimitar
|
274
283
|
shortbow: Shortbow
|
275
284
|
shortsword: Shortsword
|
285
|
+
warhammer: Warhammer
|
276
286
|
options:
|
277
287
|
automatic_roll: Roll Dice for me automatically (Computer will roll for you)
|
278
288
|
dice_roll: Toggle Manual Dice Roll ...
|
Binary file
|
data/races/dwarf.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
base_speed: 25
|
2
|
+
size: medium
|
3
|
+
darkvision: 60
|
4
|
+
attribute_bonus:
|
5
|
+
con: 2
|
6
|
+
skills:
|
7
|
+
- perception
|
8
|
+
languages:
|
9
|
+
- common
|
10
|
+
- dwarvish
|
11
|
+
race_features:
|
12
|
+
- dwarven_resilience
|
13
|
+
- stonecunning
|
14
|
+
weapon_proficiencies:
|
15
|
+
- battleaxe
|
16
|
+
- handaxe
|
17
|
+
- light_hammer
|
18
|
+
- warhammer
|
19
|
+
tool_proficiencies_choice: 1
|
20
|
+
tool_proficiencies:
|
21
|
+
- smiths_tools
|
22
|
+
- brewers_supplies
|
23
|
+
- masons_tools
|
24
|
+
subrace:
|
25
|
+
hill_dwarf:
|
26
|
+
attribute_bonus:
|
27
|
+
wis: 1
|
28
|
+
race_features:
|
29
|
+
- dwarven_toughness
|
30
|
+
mountain_dwarf:
|
31
|
+
attribute_bonus:
|
32
|
+
str: 1
|
33
|
+
weapon_proficiencies:
|
34
|
+
- light_armor
|
35
|
+
- medium_armor
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: natural_20
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joseph Dayo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -257,6 +257,7 @@ files:
|
|
257
257
|
- fixtures/corridors.yml
|
258
258
|
- fixtures/elf_rogue.yml
|
259
259
|
- fixtures/halfling_rogue.yml
|
260
|
+
- fixtures/hide_test.yml
|
260
261
|
- fixtures/high_elf_fighter.yml
|
261
262
|
- fixtures/human_fighter.yml
|
262
263
|
- fixtures/path_finding_test.yml
|
@@ -334,12 +335,14 @@ files:
|
|
334
335
|
- lib/natural_20/version.rb
|
335
336
|
- locales/en.yml
|
336
337
|
- maps/game_map.yml
|
338
|
+
- natural_20-0.1.0.gem
|
337
339
|
- natural_20.gemspec
|
338
340
|
- npcs/goblin.yml
|
339
341
|
- npcs/human_guard.yml
|
340
342
|
- npcs/ogre.yml
|
341
343
|
- npcs/owlbear.yml
|
342
344
|
- npcs/wolf.yml
|
345
|
+
- races/dwarf.yml
|
343
346
|
- races/elf.yml
|
344
347
|
- races/halfling.yml
|
345
348
|
- races/human.yml
|