natural_20 0.1.1 → 0.2.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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +13 -0
- data/README.md +1 -0
- data/bin/nat20 +2 -1
- data/bin/nat20.cmd +0 -0
- data/char_classes/wizard.yml +89 -0
- data/characters/high_elf_mage.yml +27 -0
- data/fixtures/battle_sim_objects.yml +2 -2
- data/fixtures/high_elf_mage.yml +28 -0
- data/fixtures/large_map.yml +63 -0
- data/game.yml +2 -2
- data/items/equipment.yml +30 -0
- data/items/objects.yml +33 -29
- data/items/spells.yml +58 -0
- data/items/weapons.yml +78 -18
- data/lib/CHANGELOG.md +0 -0
- data/lib/natural_20.rb +9 -0
- data/lib/natural_20/actions/action.rb +2 -2
- data/lib/natural_20/actions/attack_action.rb +76 -67
- data/lib/natural_20/actions/concerns/action_damage.rb +3 -1
- data/lib/natural_20/actions/dash_action.rb +7 -10
- data/lib/natural_20/actions/disengage_action.rb +11 -12
- data/lib/natural_20/actions/dodge_action.rb +7 -8
- data/lib/natural_20/actions/escape_grapple_action.rb +16 -18
- data/lib/natural_20/actions/first_aid_action.rb +14 -16
- data/lib/natural_20/actions/grapple_action.rb +24 -28
- data/lib/natural_20/actions/ground_interact_action.rb +1 -3
- data/lib/natural_20/actions/help_action.rb +13 -16
- data/lib/natural_20/actions/hide_action.rb +7 -9
- data/lib/natural_20/actions/interact_action.rb +12 -14
- data/lib/natural_20/actions/look_action.rb +14 -15
- data/lib/natural_20/actions/move_action.rb +9 -9
- data/lib/natural_20/actions/multiattack_action.rb +8 -9
- data/lib/natural_20/actions/prone_action.rb +4 -6
- data/lib/natural_20/actions/short_rest_action.rb +7 -8
- data/lib/natural_20/actions/shove_action.rb +20 -24
- data/lib/natural_20/actions/spell_action.rb +89 -0
- data/lib/natural_20/actions/stand_action.rb +5 -7
- data/lib/natural_20/actions/use_item_action.rb +7 -9
- data/lib/natural_20/ai_controller/standard.rb +1 -1
- data/lib/natural_20/battle.rb +8 -3
- data/lib/natural_20/cli/action_ui.rb +180 -0
- data/lib/natural_20/cli/builder/fighter_builder.rb +1 -1
- data/lib/natural_20/cli/builder/rogue_builder.rb +10 -10
- data/lib/natural_20/cli/builder/wizard_builder.rb +77 -0
- data/lib/natural_20/cli/character_builder.rb +9 -4
- data/lib/natural_20/cli/commandline_ui.rb +55 -162
- data/lib/natural_20/cli/inventory_ui.rb +4 -0
- data/lib/natural_20/cli/map_renderer.rb +7 -1
- data/lib/natural_20/concerns/attack_helper.rb +53 -0
- data/lib/natural_20/concerns/entity.rb +170 -11
- data/lib/natural_20/concerns/fighter_actions/second_wind_action.rb +7 -9
- data/lib/natural_20/concerns/fighter_class.rb +2 -2
- data/lib/natural_20/concerns/spell_attack_helper.rb +33 -0
- data/lib/natural_20/concerns/wizard_class.rb +86 -0
- data/lib/natural_20/die_roll.rb +2 -2
- data/lib/natural_20/event_manager.rb +50 -44
- data/lib/natural_20/item_library/base_item.rb +1 -1
- data/lib/natural_20/npc.rb +4 -0
- data/lib/natural_20/player_character.rb +75 -12
- data/lib/natural_20/session.rb +14 -1
- data/lib/natural_20/spell_library/firebolt.rb +72 -0
- data/lib/natural_20/spell_library/mage_armor.rb +67 -0
- data/lib/natural_20/spell_library/mage_hand.rb +2 -0
- data/lib/natural_20/spell_library/magic_missile.rb +67 -0
- data/lib/natural_20/spell_library/shield.rb +69 -0
- data/lib/natural_20/spell_library/spell.rb +31 -0
- data/lib/natural_20/utils/weapons.rb +8 -6
- data/lib/natural_20/version.rb +1 -1
- data/locales/en.yml +44 -8
- data/maps/game_map.yml +12 -2
- metadata +22 -3
@@ -9,7 +9,7 @@ class MultiattackAction < Natural20::Action
|
|
9
9
|
def build_map
|
10
10
|
OpenStruct.new({
|
11
11
|
param: nil,
|
12
|
-
next: -> { self }
|
12
|
+
next: -> { self }
|
13
13
|
})
|
14
14
|
end
|
15
15
|
|
@@ -22,20 +22,19 @@ class MultiattackAction < Natural20::Action
|
|
22
22
|
@result = [{
|
23
23
|
source: @source,
|
24
24
|
type: :multiattack,
|
25
|
-
battle: opts[:battle]
|
25
|
+
battle: opts[:battle]
|
26
26
|
}]
|
27
27
|
self
|
28
28
|
end
|
29
29
|
|
30
30
|
# @param battle [Natural20::Battle]
|
31
|
-
def apply!(battle)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@total_attacks += 2
|
36
|
-
end
|
37
|
-
|
31
|
+
def self.apply!(battle, item)
|
32
|
+
case (item[:type])
|
33
|
+
when :multiattack
|
34
|
+
@total_attacks += 2
|
38
35
|
battle.consume!(:action, 1)
|
39
36
|
end
|
37
|
+
|
38
|
+
|
40
39
|
end
|
41
40
|
end
|
@@ -27,12 +27,10 @@ class ProneAction < Natural20::Action
|
|
27
27
|
self
|
28
28
|
end
|
29
29
|
|
30
|
-
def apply!(_battle)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
item[:source].prone!
|
35
|
-
end
|
30
|
+
def apply!(_battle, item)
|
31
|
+
case (item[:type])
|
32
|
+
when :prone
|
33
|
+
item[:source].prone!
|
36
34
|
end
|
37
35
|
end
|
38
36
|
end
|
@@ -39,15 +39,14 @@ class ShortRestAction < Natural20::Action
|
|
39
39
|
end
|
40
40
|
|
41
41
|
# @param battle [Natural20::Battle]
|
42
|
-
def apply!(battle)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
entity.short_rest!(battle, prompt: true)
|
49
|
-
end
|
42
|
+
def self.apply!(battle, item)
|
43
|
+
case (item[:type])
|
44
|
+
when :short_rest
|
45
|
+
Natural20::EventManager.received_event({ source: item[:source], event: :short_rest, targets: item[:targets] })
|
46
|
+
item[:targets].each do |entity|
|
47
|
+
entity.short_rest!(battle, prompt: true)
|
50
48
|
end
|
49
|
+
battle.session.increment_game_time!(60 * 60) # increment by an hour
|
51
50
|
end
|
52
51
|
end
|
53
52
|
end
|
@@ -107,33 +107,29 @@ class ShoveAction < Natural20::Action
|
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
|
-
def apply!(battle)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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])
|
110
|
+
def self.apply!(battle, item)
|
111
|
+
case (item[:type])
|
112
|
+
when :shove
|
113
|
+
if item[:success]
|
114
|
+
if item[:knock_prone]
|
115
|
+
item[:target].prone!
|
116
|
+
elsif item[:shove_loc]
|
117
|
+
item[:battle].map.move_to!(item[:target], *item[:shove_loc], battle)
|
133
118
|
end
|
134
119
|
|
135
|
-
|
120
|
+
Natural20::EventManager.received_event(event: :shove_success,
|
121
|
+
knock_prone: item[:knock_prone],
|
122
|
+
target: item[:target], source: item[:source],
|
123
|
+
source_roll: item[:source_roll],
|
124
|
+
target_roll: item[:target_roll])
|
125
|
+
else
|
126
|
+
Natural20::EventManager.received_event(event: :shove_failure,
|
127
|
+
target: item[:target], source: item[:source],
|
128
|
+
source_roll: item[:source_roll],
|
129
|
+
target_roll: item[:target_roll])
|
136
130
|
end
|
131
|
+
|
132
|
+
battle.entity_state_for(item[:source])[:action] -= 1
|
137
133
|
end
|
138
134
|
end
|
139
135
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class SpellAction < Natural20::Action
|
2
|
+
extend Natural20::ActionDamage
|
3
|
+
|
4
|
+
attr_accessor :target, :spell_action, :spell, :other_params, :at_level
|
5
|
+
|
6
|
+
def self.can?(entity, battle, options = {})
|
7
|
+
return false unless entity.has_spells?
|
8
|
+
|
9
|
+
battle.nil? || !battle.ongoing? || can_cast?(entity, battle, options[:spell])
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param battle [Natural20::Battle]
|
13
|
+
# @param spell [Symbol]
|
14
|
+
def self.can_cast?(entity, battle, spell)
|
15
|
+
return true unless spell
|
16
|
+
|
17
|
+
spell_details = battle.session.load_spell(spell)
|
18
|
+
amt, resource = spell_details[:casting_time].split(':')
|
19
|
+
|
20
|
+
return true if resource == ('action') && battle.total_actions(entity).positive?
|
21
|
+
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.build(session, source)
|
26
|
+
action = SpellAction.new(session, source, :spell)
|
27
|
+
action.build_map
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_map
|
31
|
+
OpenStruct.new({
|
32
|
+
action: self,
|
33
|
+
param: [
|
34
|
+
{
|
35
|
+
type: :select_spell
|
36
|
+
}
|
37
|
+
],
|
38
|
+
next: lambda { |spell_choice|
|
39
|
+
spell, at_level = spell_choice
|
40
|
+
@spell = session.load_spell(spell)
|
41
|
+
raise "spell not found #{spell}" unless @spell
|
42
|
+
|
43
|
+
self.at_level = at_level
|
44
|
+
@spell_action = @spell[:spell_class].constantize.new(@source, spell, @spell)
|
45
|
+
@spell_action.build_map(self)
|
46
|
+
}
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
def resolve(_session, _map = nil, opts = {})
|
51
|
+
battle = opts[:battle]
|
52
|
+
@result = spell_action.resolve(@source, battle, self)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.apply!(battle, item)
|
57
|
+
Natural20::Spell.descendants.each do |klass|
|
58
|
+
klass.apply!(battle, item)
|
59
|
+
end
|
60
|
+
case item[:type]
|
61
|
+
when :spell_damage
|
62
|
+
damage_event(item, battle)
|
63
|
+
consume_resource(battle, item)
|
64
|
+
when :spell_miss
|
65
|
+
consume_resource(battle, item)
|
66
|
+
Natural20::EventManager.received_event({ attack_roll: item[:attack_roll],
|
67
|
+
attack_name: item[:attack_name],
|
68
|
+
advantage_mod: item[:advantage_mod],
|
69
|
+
as_reaction: !!item[:as_reaction],
|
70
|
+
adv_info: item[:adv_info],
|
71
|
+
source: item[:source], target: item[:target], event: :miss })
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param battle [Natural20::Battle]
|
76
|
+
# @param item [Hash]
|
77
|
+
def self.consume_resource(battle, item)
|
78
|
+
amt, resource = item.dig(:spell, :casting_time).split(':')
|
79
|
+
spell_level = item.dig(:spell, :level)
|
80
|
+
case resource
|
81
|
+
when 'action'
|
82
|
+
battle.consume(item[:source], :action)
|
83
|
+
when 'reaction'
|
84
|
+
battle.consume(item[:source], :reaction)
|
85
|
+
end
|
86
|
+
|
87
|
+
item[:source].consume_spell_slot!(spell_level) if spell_level.positive?
|
88
|
+
end
|
89
|
+
end
|
@@ -29,13 +29,11 @@ class StandAction < Natural20::Action
|
|
29
29
|
self
|
30
30
|
end
|
31
31
|
|
32
|
-
def apply!(battle)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
battle.entity_state_for(item[:source])[:movement] -= (item[:source].speed / 2).floor
|
38
|
-
end
|
32
|
+
def apply!(battle, item)
|
33
|
+
case (item[:type])
|
34
|
+
when :stand
|
35
|
+
item[:source].stand!
|
36
|
+
battle.consume(item[:source], :movement, (item[:source].speed / 2).floor)
|
39
37
|
end
|
40
38
|
end
|
41
39
|
|
@@ -43,15 +43,13 @@ class UseItemAction < Natural20::Action
|
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
46
|
-
def apply!(battle)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
battle.entity_state_for(item[:source])[:action] -= 1 if battle
|
54
|
-
end
|
46
|
+
def self.apply!(battle, item)
|
47
|
+
case item[:type]
|
48
|
+
when :use_item
|
49
|
+
Natural20::EventManager.received_event({ event: :use_item, source: item[:source], item: item[:item] })
|
50
|
+
item[:item].use!(item[:target], item)
|
51
|
+
item[:source].deduct_item(item[:item].name, 1) if item[:item].consumable?
|
52
|
+
battle.entity_state_for(item[:source])[:action] -= 1 if battle
|
55
53
|
end
|
56
54
|
end
|
57
55
|
end
|
data/lib/natural_20/battle.rb
CHANGED
@@ -337,7 +337,7 @@ module Natural20
|
|
337
337
|
end
|
338
338
|
|
339
339
|
# consume action resource and return if something changed
|
340
|
-
def consume!(entity, resource, qty)
|
340
|
+
def consume!(entity, resource, qty = 1)
|
341
341
|
current_qty = entity_state_for(entity)[resource.to_sym]
|
342
342
|
new_qty = [0, current_qty - qty].max
|
343
343
|
entity_state_for(entity)[resource.to_sym] = new_qty
|
@@ -505,7 +505,11 @@ module Natural20
|
|
505
505
|
return if action.nil?
|
506
506
|
|
507
507
|
# check_action_serialization(action)
|
508
|
-
action.
|
508
|
+
action.result.each do |item|
|
509
|
+
Natural20::Action.descendants.each do |klass|
|
510
|
+
klass.apply!(self, item)
|
511
|
+
end
|
512
|
+
end
|
509
513
|
|
510
514
|
case action.action_type
|
511
515
|
when :move
|
@@ -528,6 +532,7 @@ module Natural20
|
|
528
532
|
# @param resouce [Symbol]
|
529
533
|
def consume(entity, resource, qty = 1)
|
530
534
|
raise 'invalid resource' unless %i[action reaction bonus_action movement].include?(resource.to_sym)
|
535
|
+
return unless entity_state_for(entity)
|
531
536
|
|
532
537
|
entity_state_for(entity)[resource.to_sym] = [0, entity_state_for(entity)[resource.to_sym] - qty].max
|
533
538
|
end
|
@@ -535,7 +540,7 @@ module Natural20
|
|
535
540
|
protected
|
536
541
|
|
537
542
|
def t(key, options = {})
|
538
|
-
I18n.t(key, options)
|
543
|
+
I18n.t(key, **options)
|
539
544
|
end
|
540
545
|
|
541
546
|
def check_action_serialization(action)
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Natural20::ActionUI
|
2
|
+
TTY_PROMPT_PER_PAGE = 20
|
3
|
+
|
4
|
+
# Show action UI
|
5
|
+
# @param action [Natural20::Action]
|
6
|
+
# @param entity [Entity]
|
7
|
+
def action_ui(action, entity)
|
8
|
+
return :stop if action == :stop
|
9
|
+
|
10
|
+
cont = action.build_map
|
11
|
+
loop do
|
12
|
+
param = cont.param&.map do |p|
|
13
|
+
case (p[:type])
|
14
|
+
when :look
|
15
|
+
self
|
16
|
+
when :movement
|
17
|
+
move_path, jump_index = move_ui(entity, p)
|
18
|
+
return nil if move_path.nil?
|
19
|
+
|
20
|
+
[move_path, jump_index]
|
21
|
+
when :target, :select_target
|
22
|
+
targets = attack_ui(entity, action, p)
|
23
|
+
return nil if targets.nil? || targets.empty?
|
24
|
+
|
25
|
+
p[:num] > 1 ? targets : targets.first
|
26
|
+
when :select_spell
|
27
|
+
spell_slots_ui(entity)
|
28
|
+
|
29
|
+
spell = prompt.select("#{entity.name} cast Spell", per_page: TTY_PROMPT_PER_PAGE) do |q|
|
30
|
+
spell_choice(entity, battle, q)
|
31
|
+
q.choice t(:back).colorize(:blue), :back
|
32
|
+
end
|
33
|
+
|
34
|
+
return nil if spell == :back
|
35
|
+
|
36
|
+
spell
|
37
|
+
when :select_weapon
|
38
|
+
action.using || action.npc_action
|
39
|
+
when :select_item
|
40
|
+
item = prompt.select("#{entity.name} use item", per_page: TTY_PROMPT_PER_PAGE) do |menu|
|
41
|
+
entity.usable_items.each do |d|
|
42
|
+
if d[:consumable]
|
43
|
+
menu.choice "#{d[:label].colorize(:blue)} (#{d[:qty]})", d[:name]
|
44
|
+
else
|
45
|
+
menu.choice d[:label].colorize(:blue).to_s, d[:name]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
menu.choice t(:back).colorize(:blue), :back
|
49
|
+
end
|
50
|
+
|
51
|
+
return nil if item == :back
|
52
|
+
|
53
|
+
item
|
54
|
+
when :select_ground_items
|
55
|
+
selected_items = prompt.multi_select("Items on the ground around #{entity.name}") do |menu|
|
56
|
+
map.items_on_the_ground(entity).each do |ground_item|
|
57
|
+
ground, items = ground_item
|
58
|
+
items.each do |t|
|
59
|
+
item_label = t("object.#{t.label}", default: t.label)
|
60
|
+
menu.choice t('inventory.inventory_items', name: item_label, qty: t.qty), [ground, t]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
return nil if selected_items.empty?
|
66
|
+
|
67
|
+
item_selection = {}
|
68
|
+
selected_items.each do |s_item|
|
69
|
+
ground, item = s_item
|
70
|
+
|
71
|
+
qty = how_many?(item)
|
72
|
+
item_selection[ground] ||= []
|
73
|
+
item_selection[ground] << [item, qty]
|
74
|
+
end
|
75
|
+
|
76
|
+
item_selection.map do |k, v|
|
77
|
+
[k, v]
|
78
|
+
end
|
79
|
+
when :select_object
|
80
|
+
target_objects = entity.usable_objects(map, battle)
|
81
|
+
item = prompt.select("#{entity.name} interact with") do |menu|
|
82
|
+
target_objects.each do |d|
|
83
|
+
menu.choice d.name.humanize.to_s, d
|
84
|
+
end
|
85
|
+
menu.choice t(:manual_target), :manual_target
|
86
|
+
menu.choice t(:back).colorize(:blue), :back
|
87
|
+
end
|
88
|
+
|
89
|
+
return nil if item == :back
|
90
|
+
|
91
|
+
if item == :manual_target
|
92
|
+
item = target_ui(entity, num_select: 1, validation: lambda { |selected|
|
93
|
+
selected_entities = map.thing_at(*selected)
|
94
|
+
|
95
|
+
return false if selected_entities.empty?
|
96
|
+
|
97
|
+
selected_entities.detect do |selected_entity|
|
98
|
+
target_objects.include?(selected_entity)
|
99
|
+
end
|
100
|
+
}).first
|
101
|
+
end
|
102
|
+
|
103
|
+
item
|
104
|
+
when :select_items
|
105
|
+
selected_items = prompt.multi_select(p[:label], per_page: TTY_PROMPT_PER_PAGE) do |menu|
|
106
|
+
p[:items].each do |m|
|
107
|
+
item_label = t("object.#{m.label}", default: m.label)
|
108
|
+
if m.try(:equipped)
|
109
|
+
menu.choice t('inventory.equiped_items', name: item_label), m
|
110
|
+
else
|
111
|
+
menu.choice t('inventory.inventory_items', name: item_label, qty: m.qty), m
|
112
|
+
end
|
113
|
+
end
|
114
|
+
menu.choice t(:back).colorize(:blue), :back
|
115
|
+
end
|
116
|
+
|
117
|
+
return nil if selected_items.include?(:back)
|
118
|
+
|
119
|
+
selected_items = selected_items.map do |m|
|
120
|
+
count = how_many?(m)
|
121
|
+
[m, count]
|
122
|
+
end
|
123
|
+
selected_items
|
124
|
+
when :interact
|
125
|
+
object_action = prompt.select("#{entity.name} will") do |menu|
|
126
|
+
interactions = p[:target].available_interactions(entity)
|
127
|
+
class_key = p[:target].class.to_s
|
128
|
+
if interactions.is_a?(Array)
|
129
|
+
interactions.each do |k|
|
130
|
+
menu.choice t(:"object.#{class_key}.#{k}", default: k.to_s.humanize), k
|
131
|
+
end
|
132
|
+
else
|
133
|
+
interactions.each do |k, options|
|
134
|
+
label = options[:label] || t(:"object.#{class_key}.#{k}", default: k.to_s.humanize)
|
135
|
+
if options[:disabled]
|
136
|
+
menu.choice label, k, disabled: options[:disabled_text]
|
137
|
+
else
|
138
|
+
menu.choice label, k
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
menu.choice 'Back', :back
|
143
|
+
end
|
144
|
+
|
145
|
+
return nil if item == :back
|
146
|
+
|
147
|
+
object_action
|
148
|
+
when :show_inventory
|
149
|
+
inventory_ui(entity)
|
150
|
+
else
|
151
|
+
raise "unknown #{p[:type]}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
cont = cont.next.call(*param)
|
155
|
+
break if param.nil?
|
156
|
+
end
|
157
|
+
@action = cont
|
158
|
+
end
|
159
|
+
|
160
|
+
def spell_choice(entity, battle, menu)
|
161
|
+
entity.available_spells(battle).each do |k, details|
|
162
|
+
available_levels = if details[:higher_level]
|
163
|
+
(details[:level]..9).select { |lvl| entity.max_spell_slots(lvl).positive? }
|
164
|
+
else
|
165
|
+
[details[:level]]
|
166
|
+
end
|
167
|
+
available_levels.each do |spell_level|
|
168
|
+
level_str = spell_level.zero? ? 'cantrip' : "lvl. #{spell_level}"
|
169
|
+
choice_label = t(:"action.spell_choice", spell: t("spell.#{k}"), level: level_str,
|
170
|
+
description: details[:description])
|
171
|
+
if details[:disabled].empty?
|
172
|
+
menu.choice(choice_label, [k, spell_level])
|
173
|
+
else
|
174
|
+
disable_reason = details[:disabled].map { |d| t("spells.disabled.#{d}") }.join(', ')
|
175
|
+
menu.choice(choice_label, k, disabled: disable_reason)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|