natural_20 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|