diff --git a/app/commands/characters_context/avatar_attaching.rb b/app/commands/characters_context/avatar_attaching.rb new file mode 100644 index 000000000..62cfc16f5 --- /dev/null +++ b/app/commands/characters_context/avatar_attaching.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module CharactersContext + # Avatar upload branch shared by the provider update commands: at most one of + # avatar_file/avatar_url/file arrives (the contracts enforce it), and a failed + # upload must never fail the update that carried it. + module AvatarAttaching + private + + def upload_avatar(input) # rubocop: disable Metrics/AbcSize + return if input.slice(:avatar_file, :avatar_url, :file).keys.blank? + + attach_avatar_by_file.call({ character: input[:character], file: input[:avatar_file] }) if input[:avatar_file] + attach_avatar_by_url.call({ character: input[:character], url: input[:avatar_url] }) if input[:avatar_url] + return unless input[:file] + + input[:character].avatar.attach(input[:file]) + cache.push_item(item: input[:character].avatar) + rescue StandardError => _e + end + end +end diff --git a/app/commands/characters_context/character_options.rb b/app/commands/characters_context/character_options.rb new file mode 100644 index 000000000..fc982f1d0 --- /dev/null +++ b/app/commands/characters_context/character_options.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module CharactersContext + # Enumerations the provider update contracts validate arrays against. The + # dnd5, dnd2024 and TLC rule sets share every list here. + module CharacterOptions + SKILLS = %w[ + acrobatics animal arcana athletics deception history insight intimidation investigation + medicine nature perception performance persuasion religion sleight stealth survival + ].freeze + WEAPON_CORE_SKILLS = %w[light martial].freeze + ARMOR_PROFICIENCY = %w[light medium heavy shield].freeze + DAMAGE_TYPES = %w[ + bludge pierce slash acid cold fire force lighting necrotic + poison psychic radiant thunder + ].freeze + end +end diff --git a/app/commands/characters_context/class_spell_feats.rb b/app/commands/characters_context/class_spell_feats.rb new file mode 100644 index 000000000..40bb4536e --- /dev/null +++ b/app/commands/characters_context/class_spell_feats.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module CharactersContext + # dnd2024-style spell lists: a spell is a Feat row with origin `class_spell` + # (6), attached to the character and tagged with the class that prepared it, so + # dropping a class can drop exactly the spells it brought. Including commands + # supply the content scope and the homebrew feat types their provider owns. + module ClassSpellFeats + private + + def spell_feats_scope = raise(NotImplementedError) + def homebrew_feat_types = raise(NotImplementedError) + + def refresh_class_spells(input) + character = input[:character] + input[:added_classes].each do |added_class| + learn_class_spells(character: character, class_name: added_class, user: character.user) + end + input[:removed_classes].each { |removed_class| forget_class_spells(character: character, class_name: removed_class) } + end + + def learn_class_spells(character:, class_name:, user:) + return if ::Dnd2024::Character::CLASSES_KNOW_SPELLS_LIST.exclude?(class_name) + + relation = spell_feats_scope.where(origin: 6).where('origin_values && ?', "{#{class_name}}") + spells = + relation.where(user_id: [nil, user.id]).or(relation.where(id: homebrew_feat_ids(user))) + .map do |feat| + { + character_id: character.id, + feat_id: feat.id, + ready_to_use: false, + value: { prepared_by: class_name } + } + end + ::Character::Feat.upsert_all(spells) if spells.any? + end + + def forget_class_spells(character:, class_name:) + character.feats.where("value -> 'prepared_by' ? :prepared_by", prepared_by: class_name).delete_all + end + + def homebrew_feat_ids(user) + ::Homebrew::Book::Item + .where(homebrew_book_id: ::User::Book.where(user_id: user).select(:homebrew_book_id)) + .where(itemable_type: homebrew_feat_types) + .pluck(:itemable_id) + end + end +end diff --git a/app/commands/characters_context/coins_syncing.rb b/app/commands/characters_context/coins_syncing.rb new file mode 100644 index 000000000..ab93685ab --- /dev/null +++ b/app/commands/characters_context/coins_syncing.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module CharactersContext + # `money` (total copper) and `coins` (gold/silver/copper) are two views of the + # same purse, and a client sends whichever one its screen edits. Shared by the + # provider update commands so both views persist in step. + module CoinsSyncing + private + + def sync_coins_and_money(input) + if input.key?(:money) + gold, modulus = input[:money].divmod(100) + silver, copper = modulus.divmod(10) + input[:coins] = { copper: copper, silver: silver, gold: gold } + elsif input.key?(:coins) + input[:money] = (input.dig(:coins, :gold) * 100) + (input.dig(:coins, :silver) * 10) + input.dig(:coins, :copper) + end + end + end +end diff --git a/app/commands/characters_context/dnd2024/update_command.rb b/app/commands/characters_context/dnd2024/update_command.rb index 29cbcf26d..d61922bd2 100644 --- a/app/commands/characters_context/dnd2024/update_command.rb +++ b/app/commands/characters_context/dnd2024/update_command.rb @@ -3,6 +3,10 @@ module CharactersContext module Dnd2024 class UpdateCommand < BaseCommand + include CharactersContext::AvatarAttaching + include CharactersContext::CharacterOptions + include CharactersContext::ClassSpellFeats + include CharactersContext::CoinsSyncing include Deps[ attach_avatar_by_url: 'commands.image_processing.attach_avatar_by_url', attach_avatar_by_file: 'commands.image_processing.attach_avatar_by_file', @@ -10,14 +14,6 @@ class UpdateCommand < BaseCommand cache: 'cache.avatars' ] - SKILLS = %w[ - acrobatics animal arcana athletics deception history insight intimidation investigation - medicine nature perception performance persuasion religion sleight stealth survival - ].freeze - WEAPON_CORE_SKILLS = %w[light martial].freeze - ARMOR_PROFICIENCY = %w[light medium heavy shield].freeze - DAMAGE_TYPES = %w[bludge pierce slash acid cold fire force lighting necrotic poison psychic radiant thunder].freeze - # rubocop: disable Metrics/BlockLength use_contract do config.messages.namespace = :dnd5_character @@ -103,7 +99,7 @@ class UpdateCommand < BaseCommand def lock_key(input) = "character_update_#{input[:character].id}" def lock_time = 0 - def do_prepare(input) # rubocop: disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength + def do_prepare(input) # rubocop: disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength %i[classes abilities health coins energy spent_spell_slots spent_hit_dice].each do |key| input[key]&.transform_values!(&:to_i) end @@ -118,13 +114,7 @@ def do_prepare(input) # rubocop: disable Metrics/AbcSize, Metrics/PerceivedCompl end end - if input.key?(:money) - gold, modulus = input[:money].divmod(100) - silver, copper = modulus.divmod(10) - input[:coins] = { copper: copper, silver: silver, gold: gold } - elsif input.key?(:coins) - input[:money] = (input.dig(:coins, :gold) * 100) + (input.dig(:coins, :silver) * 10) + input.dig(:coins, :copper) - end + sync_coins_and_money(input) if input.key?(:abilities) input[:ability_boosts] = 0 @@ -149,53 +139,14 @@ def do_persist(input) # rubocop: disable Metrics/AbcSize if %i[classes subclasses selected_features selected_feats].intersect?(input.keys) refresh_feats.call(character: input[:character]) end - refresh_spells(input) if input[:classes] + refresh_class_spells(input) if input[:classes] upload_avatar(input) { result: input[:character] } end - def refresh_spells(input) # rubocop: disable Metrics/AbcSize - input[:added_classes].each do |added_class| - next if ::Dnd2024::Character::CLASSES_KNOW_SPELLS_LIST.exclude?(added_class) - - relation = ::Dnd2024::Feat.where(origin: 6).where('origin_values && ?', "{#{added_class}}") - spells = - relation.where(user_id: [nil, input[:character].user_id]).or(relation.where(id: homebrew_item_ids(input))) - .map do |feat| - { - character_id: input[:character].id, - feat_id: feat.id, - ready_to_use: false, - value: { prepared_by: added_class } - } - end - ::Character::Feat.upsert_all(spells) if spells.any? - end - - input[:removed_classes].each do |removed_class| - input[:character].feats.where("value -> 'prepared_by' ? :prepared_by", prepared_by: removed_class).delete_all - end - end - - def homebrew_item_ids(input) - ::Homebrew::Book::Item - .where(homebrew_book_id: ::User::Book.where(user_id: input[:character].user).select(:homebrew_book_id)) - .where(itemable_type: 'Dnd2024::Feat') - .pluck(:itemable_id) - end - - def upload_avatar(input) # rubocop: disable Metrics/AbcSize - return if input.slice(:avatar_file, :avatar_url, :file).keys.blank? - - attach_avatar_by_file.call({ character: input[:character], file: input[:avatar_file] }) if input[:avatar_file] - attach_avatar_by_url.call({ character: input[:character], url: input[:avatar_url] }) if input[:avatar_url] - return unless input[:file] - - input[:character].avatar.attach(input[:file]) - cache.push_item(item: input[:character].avatar) - rescue StandardError => _e - end + def spell_feats_scope = ::Dnd2024::Feat + def homebrew_feat_types = 'Dnd2024::Feat' end end end diff --git a/app/commands/characters_context/dnd5/update_command.rb b/app/commands/characters_context/dnd5/update_command.rb index fbfc7deb3..802e4895c 100644 --- a/app/commands/characters_context/dnd5/update_command.rb +++ b/app/commands/characters_context/dnd5/update_command.rb @@ -3,6 +3,9 @@ module CharactersContext module Dnd5 class UpdateCommand < BaseCommand + include CharactersContext::AvatarAttaching + include CharactersContext::CharacterOptions + include CharactersContext::CoinsSyncing include Deps[ attach_avatar_by_url: 'commands.image_processing.attach_avatar_by_url', attach_avatar_by_file: 'commands.image_processing.attach_avatar_by_file', @@ -10,17 +13,6 @@ class UpdateCommand < BaseCommand cache: 'cache.avatars' ] - SKILLS = %w[ - acrobatics animal arcana athletics deception history insight intimidation investigation - medicine nature perception performance persuasion religion sleight stealth survival - ].freeze - WEAPON_CORE_SKILLS = %w[light martial].freeze - ARMOR_PROFICIENCY = %w[light medium heavy shield].freeze - DAMAGE_TYPES = %w[ - bludge pierce slash acid cold fire force lighting necrotic - poison psychic radiant thunder - ].freeze - # rubocop: disable Metrics/BlockLength use_contract do config.messages.namespace = :dnd5_character @@ -104,7 +96,7 @@ class UpdateCommand < BaseCommand def lock_key(input) = "character_update_#{input[:character].id}" def lock_time = 0 - # rubocop: disable Metrics/AbcSize, Metrics/PerceivedComplexity + # rubocop: disable Metrics/AbcSize def do_prepare(input) if input[:classes] input[:level] = input[:classes].values.sum(&:to_i) @@ -122,13 +114,7 @@ def do_prepare(input) input[:hit_dice][::Dnd5::Character::HIT_DICES[key]] += class_level end - if input.key?(:money) - gold, modulus = input[:money].divmod(100) - silver, copper = modulus.divmod(10) - input[:coins] = { copper: copper, silver: silver, gold: gold } - elsif input.key?(:coins) - input[:money] = (input.dig(:coins, :gold) * 100) + (input.dig(:coins, :silver) * 10) + input.dig(:coins, :copper) - end + sync_coins_and_money(input) end def do_persist(input) @@ -145,7 +131,7 @@ def do_persist(input) { result: input[:character] } end - # rubocop: enable Metrics/AbcSize, Metrics/PerceivedComplexity + # rubocop: enable Metrics/AbcSize def refresh_spells(input) input[:added_classes].each do |added_class| @@ -166,18 +152,6 @@ def refresh_spells(input) input[:character].spells.where("data -> 'prepared_by' ? :prepared_by", prepared_by: removed_class).delete_all end end - - def upload_avatar(input) # rubocop: disable Metrics/AbcSize - return if input.slice(:avatar_file, :avatar_url, :file).keys.blank? - - attach_avatar_by_file.call({ character: input[:character], file: input[:avatar_file] }) if input[:avatar_file] - attach_avatar_by_url.call({ character: input[:character], url: input[:avatar_url] }) if input[:avatar_url] - return unless input[:file] - - input[:character].avatar.attach(input[:file]) - cache.push_item(item: input[:character].avatar) - rescue StandardError => _e - end end end end diff --git a/app/commands/characters_context/tlc/update_command.rb b/app/commands/characters_context/tlc/update_command.rb index 234e52c28..3ae64b37c 100644 --- a/app/commands/characters_context/tlc/update_command.rb +++ b/app/commands/characters_context/tlc/update_command.rb @@ -9,6 +9,10 @@ module Tlc # eval_variables / description_eval_variables (Ruby-eval'd feat columns — # plan §Security T4). Config-derived enums read the dnd2024 baseline (plan P4). class UpdateCommand < BaseCommand + include CharactersContext::AvatarAttaching + include CharactersContext::CharacterOptions + include CharactersContext::ClassSpellFeats + include CharactersContext::CoinsSyncing include Deps[ attach_avatar_by_url: 'commands.image_processing.attach_avatar_by_url', attach_avatar_by_file: 'commands.image_processing.attach_avatar_by_file', @@ -16,14 +20,6 @@ class UpdateCommand < BaseCommand cache: 'cache.avatars' ] - SKILLS = %w[ - acrobatics animal arcana athletics deception history insight intimidation investigation - medicine nature perception performance persuasion religion sleight stealth survival - ].freeze - WEAPON_CORE_SKILLS = %w[light martial].freeze - ARMOR_PROFICIENCY = %w[light medium heavy shield].freeze - DAMAGE_TYPES = %w[bludge pierce slash acid cold fire force lighting necrotic poison psychic radiant thunder].freeze - # plan §Security threats 2/3: bound + dedupe the JSONB trait array. SELECTED_TRAITS_CAP = 10 @@ -168,13 +164,7 @@ def do_prepare(input) # rubocop: disable Metrics/AbcSize, Metrics/PerceivedCompl end end - if input.key?(:money) - gold, modulus = input[:money].divmod(100) - silver, copper = modulus.divmod(10) - input[:coins] = { copper: copper, silver: silver, gold: gold } - elsif input.key?(:coins) - input[:money] = (input.dig(:coins, :gold) * 100) + (input.dig(:coins, :silver) * 10) + input.dig(:coins, :copper) - end + sync_coins_and_money(input) if input.key?(:abilities) input[:ability_boosts] = 0 @@ -199,53 +189,15 @@ def do_persist(input) # rubocop: disable Metrics/AbcSize if %i[classes subclasses selected_features selected_feats].intersect?(input.keys) refresh_feats.call(character: input[:character]) end - refresh_spells(input) if input[:classes] + refresh_class_spells(input) if input[:classes] upload_avatar(input) { result: input[:character] } end - def refresh_spells(input) # rubocop: disable Metrics/AbcSize - input[:added_classes].each do |added_class| - next if ::Dnd2024::Character::CLASSES_KNOW_SPELLS_LIST.exclude?(added_class) - - relation = ::Feat.tlc_content.where(origin: 6).where('origin_values && ?', "{#{added_class}}") - spells = - relation.where(user_id: [nil, input[:character].user_id]).or(relation.where(id: homebrew_item_ids(input))) - .map do |feat| - { - character_id: input[:character].id, - feat_id: feat.id, - ready_to_use: false, - value: { prepared_by: added_class } - } - end - ::Character::Feat.upsert_all(spells) if spells.any? - end - - input[:removed_classes].each do |removed_class| - input[:character].feats.where("value -> 'prepared_by' ? :prepared_by", prepared_by: removed_class).delete_all - end - end - - def homebrew_item_ids(input) - ::Homebrew::Book::Item - .where(homebrew_book_id: ::User::Book.where(user_id: input[:character].user).select(:homebrew_book_id)) - .where(itemable_type: %w[Dnd2024::Feat Tlc::Feat]) - .pluck(:itemable_id) - end - - def upload_avatar(input) # rubocop: disable Metrics/AbcSize - return if input.slice(:avatar_file, :avatar_url, :file).keys.blank? - - attach_avatar_by_file.call({ character: input[:character], file: input[:avatar_file] }) if input[:avatar_file] - attach_avatar_by_url.call({ character: input[:character], url: input[:avatar_url] }) if input[:avatar_url] - return unless input[:file] - - input[:character].avatar.attach(input[:file]) - cache.push_item(item: input[:character].avatar) - rescue StandardError => _e - end + # TLC reads the union content scope, and owns both dnd2024 and tlc homebrew feats. + def spell_feats_scope = ::Feat.tlc_content + def homebrew_feat_types = %w[Dnd2024::Feat Tlc::Feat] end end end diff --git a/app/decorators/dnd5_character/class_decorate_wrapper.rb b/app/decorators/dnd5_character/class_decorate_wrapper.rb index 1eee8c7c9..993628a66 100644 --- a/app/decorators/dnd5_character/class_decorate_wrapper.rb +++ b/app/decorators/dnd5_character/class_decorate_wrapper.rb @@ -2,28 +2,7 @@ module Dnd5Character class ClassDecorateWrapper < ApplicationDecorateWrapper - SPELL_SLOTS = { - 1 => { 1 => 2 }, - 2 => { 1 => 3 }, - 3 => { 1 => 4, 2 => 2 }, - 4 => { 1 => 4, 2 => 3 }, - 5 => { 1 => 4, 2 => 3, 3 => 2 }, - 6 => { 1 => 4, 2 => 3, 3 => 3 }, - 7 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 8 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 9 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 10 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 }, - 11 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1 }, - 12 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1 }, - 13 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1 }, - 14 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1 }, - 15 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1 }, - 16 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1 }, - 17 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1, 9 => 1 }, - 18 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 1, 7 => 1, 8 => 1, 9 => 1 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 2, 7 => 1, 8 => 1, 9 => 1 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 2, 7 => 2, 8 => 1, 9 => 1 } - }.freeze + SPELL_SLOTS = SpellSlots::FULL_CASTER def spells_slots @spells_slots ||= diff --git a/app/decorators/dnd5_character/classes/artificer_decorator.rb b/app/decorators/dnd5_character/classes/artificer_decorator.rb index 9c9d72b31..1d03c8e12 100644 --- a/app/decorators/dnd5_character/classes/artificer_decorator.rb +++ b/app/decorators/dnd5_character/classes/artificer_decorator.rb @@ -3,28 +3,7 @@ module Dnd5Character module Classes class ArtificerDecorator < ApplicationDecorator - SPELL_SLOTS = { - 1 => { 1 => 2 }, - 2 => { 1 => 2 }, - 3 => { 1 => 3 }, - 4 => { 1 => 3 }, - 5 => { 1 => 4, 2 => 2 }, - 6 => { 1 => 4, 2 => 2 }, - 7 => { 1 => 4, 2 => 3 }, - 8 => { 1 => 4, 2 => 3 }, - 9 => { 1 => 4, 2 => 3, 3 => 2 }, - 10 => { 1 => 4, 2 => 3, 3 => 2 }, - 11 => { 1 => 4, 2 => 3, 3 => 3 }, - 12 => { 1 => 4, 2 => 3, 3 => 3 }, - 13 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 14 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 15 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 16 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 17 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 18 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 } - }.freeze + SPELL_SLOTS = SpellSlots::HALF_CASTER CLASS_SAVE_DC = %w[con int].freeze def class_save_dc diff --git a/app/decorators/dnd5_character/classes/bard_decorator.rb b/app/decorators/dnd5_character/classes/bard_decorator.rb index 9b83b9c8c..47f342545 100644 --- a/app/decorators/dnd5_character/classes/bard_decorator.rb +++ b/app/decorators/dnd5_character/classes/bard_decorator.rb @@ -26,7 +26,7 @@ def spell_classes end def spells_slots - @spells_slots ||= ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level] + @spells_slots ||= SpellSlots::FULL_CASTER[class_level] end private @@ -55,7 +55,7 @@ def spells_amount end def max_spell_level - ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators/dnd5_character/classes/cleric_decorator.rb b/app/decorators/dnd5_character/classes/cleric_decorator.rb index f5c3867b2..0e0bfa2f5 100644 --- a/app/decorators/dnd5_character/classes/cleric_decorator.rb +++ b/app/decorators/dnd5_character/classes/cleric_decorator.rb @@ -27,7 +27,7 @@ def spell_classes # rubocop: enable Metrics/AbcSize def spells_slots - @spells_slots ||= ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level] + @spells_slots ||= SpellSlots::FULL_CASTER[class_level] end private @@ -44,7 +44,7 @@ def cantrips_amount end def max_spell_level - ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators/dnd5_character/classes/druid_decorator.rb b/app/decorators/dnd5_character/classes/druid_decorator.rb index 01916e96c..ddfb02aba 100644 --- a/app/decorators/dnd5_character/classes/druid_decorator.rb +++ b/app/decorators/dnd5_character/classes/druid_decorator.rb @@ -27,7 +27,7 @@ def spell_classes # rubocop: enable Metrics/AbcSize def spells_slots - @spells_slots ||= ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level] + @spells_slots ||= SpellSlots::FULL_CASTER[class_level] end private @@ -44,7 +44,7 @@ def cantrips_amount end def max_spell_level - ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators/dnd5_character/classes/sorcerer_decorator.rb b/app/decorators/dnd5_character/classes/sorcerer_decorator.rb index 34557d72d..7d4cca158 100644 --- a/app/decorators/dnd5_character/classes/sorcerer_decorator.rb +++ b/app/decorators/dnd5_character/classes/sorcerer_decorator.rb @@ -26,7 +26,7 @@ def spell_classes end def spells_slots - @spells_slots ||= ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level] + @spells_slots ||= SpellSlots::FULL_CASTER[class_level] end private @@ -52,7 +52,7 @@ def spells_amount end def max_spell_level - ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators/dnd5_character/classes/warlock_decorator.rb b/app/decorators/dnd5_character/classes/warlock_decorator.rb index 3797491c4..090a46dc5 100644 --- a/app/decorators/dnd5_character/classes/warlock_decorator.rb +++ b/app/decorators/dnd5_character/classes/warlock_decorator.rb @@ -3,28 +3,7 @@ module Dnd5Character module Classes class WarlockDecorator < ApplicationDecorator - SPELL_SLOTS = { - 1 => { 1 => 1 }, - 2 => { 1 => 2 }, - 3 => { 1 => 0, 2 => 2 }, - 4 => { 1 => 0, 2 => 2 }, - 5 => { 1 => 0, 2 => 0, 3 => 2 }, - 6 => { 1 => 0, 2 => 0, 3 => 2 }, - 7 => { 1 => 0, 2 => 0, 3 => 0, 4 => 2 }, - 8 => { 1 => 0, 2 => 0, 3 => 0, 4 => 2 }, - 9 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 2 }, - 10 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 2 }, - 11 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0 }, - 12 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0 }, - 13 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0 }, - 14 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0 }, - 15 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0, 8 => 0 }, - 16 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0, 8 => 0 }, - 17 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 18 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 19 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 20 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 } - }.freeze + SPELL_SLOTS = SpellSlots::WARLOCK CLASS_SAVE_DC = %w[wis cha].freeze def class_save_dc diff --git a/app/decorators/dnd5_character/classes/wizard_decorator.rb b/app/decorators/dnd5_character/classes/wizard_decorator.rb index af3f0d329..6cbb4ba3d 100644 --- a/app/decorators/dnd5_character/classes/wizard_decorator.rb +++ b/app/decorators/dnd5_character/classes/wizard_decorator.rb @@ -27,7 +27,7 @@ def spell_classes # rubocop: enable Metrics/AbcSize def spells_slots - @spells_slots ||= ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level] + @spells_slots ||= SpellSlots::FULL_CASTER[class_level] end private @@ -44,7 +44,7 @@ def cantrips_amount end def max_spell_level - ::Dnd5Character::ClassDecorateWrapper::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators_v2/dnd2024/classes/artificer_decorator.rb b/app/decorators_v2/dnd2024/classes/artificer_decorator.rb index 291329c27..1ce787878 100644 --- a/app/decorators_v2/dnd2024/classes/artificer_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/artificer_decorator.rb @@ -3,28 +3,7 @@ module Dnd2024 module Classes class ArtificerDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 2 }, - 2 => { 1 => 2 }, - 3 => { 1 => 3 }, - 4 => { 1 => 3 }, - 5 => { 1 => 4, 2 => 2 }, - 6 => { 1 => 4, 2 => 2 }, - 7 => { 1 => 4, 2 => 3 }, - 8 => { 1 => 4, 2 => 3 }, - 9 => { 1 => 4, 2 => 3, 3 => 2 }, - 10 => { 1 => 4, 2 => 3, 3 => 2 }, - 11 => { 1 => 4, 2 => 3, 3 => 3 }, - 12 => { 1 => 4, 2 => 3, 3 => 3 }, - 13 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 14 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 15 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 16 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 17 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 18 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 } - }.freeze + SPELL_SLOTS = SpellSlots::HALF_CASTER CLASS_SAVE_DC = %w[con int].freeze def call(result:) diff --git a/app/decorators_v2/dnd2024/classes/bard_decorator.rb b/app/decorators_v2/dnd2024/classes/bard_decorator.rb index 23942b175..886b6f606 100644 --- a/app/decorators_v2/dnd2024/classes/bard_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/bard_decorator.rb @@ -28,7 +28,7 @@ def spell_class_info end def spells_slots - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level] || ::Dnd2024::SubclassDecorator::SPELL_SLOTS[20] + SpellSlots::FULL_CASTER[class_level] || SpellSlots::FULL_CASTER[20] end def static_spells @@ -61,7 +61,7 @@ def prepared_spells_amount end def max_spell_level - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end def static_spell_attributes diff --git a/app/decorators_v2/dnd2024/classes/cleric_decorator.rb b/app/decorators_v2/dnd2024/classes/cleric_decorator.rb index 24a96266b..48f9e23c2 100644 --- a/app/decorators_v2/dnd2024/classes/cleric_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/cleric_decorator.rb @@ -28,7 +28,7 @@ def spell_class_info end def spells_slots - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level] || ::Dnd2024::SubclassDecorator::SPELL_SLOTS[20] + SpellSlots::FULL_CASTER[class_level] || SpellSlots::FULL_CASTER[20] end def class_level @@ -54,7 +54,7 @@ def prepared_spells_amount end def max_spell_level - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators_v2/dnd2024/classes/druid_decorator.rb b/app/decorators_v2/dnd2024/classes/druid_decorator.rb index 9eb68d877..22e5c06ec 100644 --- a/app/decorators_v2/dnd2024/classes/druid_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/druid_decorator.rb @@ -28,7 +28,7 @@ def spell_class_info end def spells_slots - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level] || ::Dnd2024::SubclassDecorator::SPELL_SLOTS[20] + SpellSlots::FULL_CASTER[class_level] || SpellSlots::FULL_CASTER[20] end def find_static_spells @@ -61,7 +61,7 @@ def prepared_spells_amount end def max_spell_level - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators_v2/dnd2024/classes/paladin_decorator.rb b/app/decorators_v2/dnd2024/classes/paladin_decorator.rb index 573c29982..5e853e7b7 100644 --- a/app/decorators_v2/dnd2024/classes/paladin_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/paladin_decorator.rb @@ -3,28 +3,7 @@ module Dnd2024 module Classes class PaladinDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 2 }, - 2 => { 1 => 2 }, - 3 => { 1 => 3 }, - 4 => { 1 => 3 }, - 5 => { 1 => 4, 2 => 2 }, - 6 => { 1 => 4, 2 => 2 }, - 7 => { 1 => 4, 2 => 3 }, - 8 => { 1 => 4, 2 => 3 }, - 9 => { 1 => 4, 2 => 3, 3 => 2 }, - 10 => { 1 => 4, 2 => 3, 3 => 2 }, - 11 => { 1 => 4, 2 => 3, 3 => 3 }, - 12 => { 1 => 4, 2 => 3, 3 => 3 }, - 13 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 14 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 15 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 16 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 17 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 18 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 } - }.freeze + SPELL_SLOTS = SpellSlots::HALF_CASTER CLASS_SAVE_DC = %w[wis cha].freeze def call(result:) diff --git a/app/decorators_v2/dnd2024/classes/sorcerer_decorator.rb b/app/decorators_v2/dnd2024/classes/sorcerer_decorator.rb index 76955f4c3..8707d1d2f 100644 --- a/app/decorators_v2/dnd2024/classes/sorcerer_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/sorcerer_decorator.rb @@ -28,7 +28,7 @@ def find_static_spells end def spells_slots - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level] || ::Dnd2024::SubclassDecorator::SPELL_SLOTS[20] + SpellSlots::FULL_CASTER[class_level] || SpellSlots::FULL_CASTER[20] end def class_level @@ -56,7 +56,7 @@ def prepared_spells_amount # rubocop: enable Metrics/AbcSize def max_spell_level - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators_v2/dnd2024/classes/warlock_decorator.rb b/app/decorators_v2/dnd2024/classes/warlock_decorator.rb index fa7b32ba3..e661a7b2e 100644 --- a/app/decorators_v2/dnd2024/classes/warlock_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/warlock_decorator.rb @@ -3,28 +3,7 @@ module Dnd2024 module Classes class WarlockDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 1 }, - 2 => { 1 => 2 }, - 3 => { 1 => 0, 2 => 2 }, - 4 => { 1 => 0, 2 => 2 }, - 5 => { 1 => 0, 2 => 0, 3 => 2 }, - 6 => { 1 => 0, 2 => 0, 3 => 2 }, - 7 => { 1 => 0, 2 => 0, 3 => 0, 4 => 2 }, - 8 => { 1 => 0, 2 => 0, 3 => 0, 4 => 2 }, - 9 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 2 }, - 10 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 2 }, - 11 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0 }, - 12 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0 }, - 13 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0 }, - 14 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0 }, - 15 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0, 8 => 0 }, - 16 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 3, 6 => 0, 7 => 0, 8 => 0 }, - 17 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 18 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 19 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 }, - 20 => { 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 4, 6 => 0, 7 => 0, 8 => 0, 9 => 0 } - }.freeze + SPELL_SLOTS = SpellSlots::WARLOCK CLASS_SAVE_DC = %w[wis cha].freeze def call(result:) diff --git a/app/decorators_v2/dnd2024/classes/wizard_decorator.rb b/app/decorators_v2/dnd2024/classes/wizard_decorator.rb index 012033ad4..baba174a1 100644 --- a/app/decorators_v2/dnd2024/classes/wizard_decorator.rb +++ b/app/decorators_v2/dnd2024/classes/wizard_decorator.rb @@ -27,7 +27,7 @@ def spell_class_info end def spells_slots - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level] || ::Dnd2024::SubclassDecorator::SPELL_SLOTS[20] + SpellSlots::FULL_CASTER[class_level] || SpellSlots::FULL_CASTER[20] end def class_level @@ -51,7 +51,7 @@ def prepared_spells_amount end def max_spell_level - ::Dnd2024::SubclassDecorator::SPELL_SLOTS[class_level].keys.max + SpellSlots::FULL_CASTER[class_level].keys.max end end end diff --git a/app/decorators_v2/dnd2024/subclass_decorator.rb b/app/decorators_v2/dnd2024/subclass_decorator.rb index 88003c20b..cbe4e133e 100644 --- a/app/decorators_v2/dnd2024/subclass_decorator.rb +++ b/app/decorators_v2/dnd2024/subclass_decorator.rb @@ -2,28 +2,7 @@ module Dnd2024 class SubclassDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 2 }, - 2 => { 1 => 3 }, - 3 => { 1 => 4, 2 => 2 }, - 4 => { 1 => 4, 2 => 3 }, - 5 => { 1 => 4, 2 => 3, 3 => 2 }, - 6 => { 1 => 4, 2 => 3, 3 => 3 }, - 7 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 8 => { 1 => 4, 2 => 3, 3 => 3, 4 => 2 }, - 9 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 1 }, - 10 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2 }, - 11 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1 }, - 12 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1 }, - 13 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1 }, - 14 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1 }, - 15 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1 }, - 16 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1 }, - 17 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 2, 6 => 1, 7 => 1, 8 => 1, 9 => 1 }, - 18 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 1, 7 => 1, 8 => 1, 9 => 1 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 2, 7 => 1, 8 => 1, 9 => 1 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 3, 5 => 3, 6 => 2, 7 => 2, 8 => 1, 9 => 1 } - }.freeze + SPELL_SLOTS = SpellSlots::FULL_CASTER def call(result:) subclass_keys = result['subclasses'].values.compact diff --git a/app/decorators_v2/dnd2024/subclasses/arcane_trickster_decorator.rb b/app/decorators_v2/dnd2024/subclasses/arcane_trickster_decorator.rb index 669ce6c0e..78850ddcb 100644 --- a/app/decorators_v2/dnd2024/subclasses/arcane_trickster_decorator.rb +++ b/app/decorators_v2/dnd2024/subclasses/arcane_trickster_decorator.rb @@ -2,78 +2,10 @@ module Dnd2024 module Subclasses - class ArcaneTricksterDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 0 }, - 2 => { 1 => 0 }, - 3 => { 1 => 2 }, - 4 => { 1 => 3 }, - 5 => { 1 => 3 }, - 6 => { 1 => 3 }, - 7 => { 1 => 4, 2 => 2 }, - 8 => { 1 => 4, 2 => 2 }, - 9 => { 1 => 4, 2 => 2 }, - 10 => { 1 => 4, 2 => 3 }, - 11 => { 1 => 4, 2 => 3 }, - 12 => { 1 => 4, 2 => 3 }, - 13 => { 1 => 4, 2 => 3, 3 => 2 }, - 14 => { 1 => 4, 2 => 3, 3 => 2 }, - 15 => { 1 => 4, 2 => 3, 3 => 2 }, - 16 => { 1 => 4, 2 => 3, 3 => 3 }, - 17 => { 1 => 4, 2 => 3, 3 => 3 }, - 18 => { 1 => 4, 2 => 3, 3 => 3 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 } - }.freeze - - def call(result:) - @result = result - @result['spell_classes']['rogue'] = spell_class_info - @result['spells_slots'] = SPELL_SLOTS[class_level] || SPELL_SLOTS[20] - @result - end - + class ArcaneTricksterDecorator < ThirdCasterDecorator private - def spell_class_info - { - save_dc: 8 + proficiency_bonus + modifiers['int'], - attack_bonus: proficiency_bonus + modifiers['int'], - cantrips_amount: cantrips_amount, - max_spell_level: max_spell_level, - prepared_spells_amount: prepared_spells_amount, - multiclass_spell_level: class_level / 3 - } - end - - def class_level - @class_level ||= classes['rogue'] - end - - def cantrips_amount - return 3 if class_level >= 10 - - 2 - end - - def prepared_spells_amount # rubocop: disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity - return 13 if class_level >= 20 - return 12 if class_level >= 19 - return 11 if class_level >= 16 - return 10 if class_level >= 14 - return 9 if class_level >= 13 - return 8 if class_level >= 11 - return 7 if class_level >= 10 - return 6 if class_level >= 8 - return 5 if class_level >= 7 - return 4 if class_level >= 4 - - 3 - end - - def max_spell_level - SPELL_SLOTS[class_level].keys.max - end + def spellcasting_class = 'rogue' end end end diff --git a/app/decorators_v2/dnd2024/subclasses/eldritch_knight_decorator.rb b/app/decorators_v2/dnd2024/subclasses/eldritch_knight_decorator.rb index f1e35c5dc..e2814ff7c 100644 --- a/app/decorators_v2/dnd2024/subclasses/eldritch_knight_decorator.rb +++ b/app/decorators_v2/dnd2024/subclasses/eldritch_knight_decorator.rb @@ -2,78 +2,10 @@ module Dnd2024 module Subclasses - class EldritchKnightDecorator < ApplicationDecoratorV2 - SPELL_SLOTS = { - 1 => { 1 => 0 }, - 2 => { 1 => 0 }, - 3 => { 1 => 2 }, - 4 => { 1 => 3 }, - 5 => { 1 => 3 }, - 6 => { 1 => 3 }, - 7 => { 1 => 4, 2 => 2 }, - 8 => { 1 => 4, 2 => 2 }, - 9 => { 1 => 4, 2 => 2 }, - 10 => { 1 => 4, 2 => 3 }, - 11 => { 1 => 4, 2 => 3 }, - 12 => { 1 => 4, 2 => 3 }, - 13 => { 1 => 4, 2 => 3, 3 => 2 }, - 14 => { 1 => 4, 2 => 3, 3 => 2 }, - 15 => { 1 => 4, 2 => 3, 3 => 2 }, - 16 => { 1 => 4, 2 => 3, 3 => 3 }, - 17 => { 1 => 4, 2 => 3, 3 => 3 }, - 18 => { 1 => 4, 2 => 3, 3 => 3 }, - 19 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 }, - 20 => { 1 => 4, 2 => 3, 3 => 3, 4 => 1 } - }.freeze - - def call(result:) - @result = result - @result['spell_classes']['fighter'] = spell_class_info - @result['spells_slots'] = SPELL_SLOTS[class_level] || SPELL_SLOTS[20] - @result - end - + class EldritchKnightDecorator < ThirdCasterDecorator private - def spell_class_info - { - save_dc: 8 + proficiency_bonus + modifiers['int'], - attack_bonus: proficiency_bonus + modifiers['int'], - cantrips_amount: cantrips_amount, - max_spell_level: max_spell_level, - prepared_spells_amount: prepared_spells_amount, - multiclass_spell_level: class_level / 3 - } - end - - def class_level - @class_level ||= classes['fighter'] - end - - def cantrips_amount - return 3 if class_level >= 10 - - 2 - end - - def prepared_spells_amount # rubocop: disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity - return 13 if class_level >= 20 - return 12 if class_level >= 19 - return 11 if class_level >= 16 - return 10 if class_level >= 14 - return 9 if class_level >= 13 - return 8 if class_level >= 11 - return 7 if class_level >= 10 - return 6 if class_level >= 8 - return 5 if class_level >= 7 - return 4 if class_level >= 4 - - 3 - end - - def max_spell_level - SPELL_SLOTS[class_level].keys.max - end + def spellcasting_class = 'fighter' end end end diff --git a/app/decorators_v2/dnd2024/subclasses/third_caster_decorator.rb b/app/decorators_v2/dnd2024/subclasses/third_caster_decorator.rb new file mode 100644 index 000000000..68b3aa7c0 --- /dev/null +++ b/app/decorators_v2/dnd2024/subclasses/third_caster_decorator.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Dnd2024 + module Subclasses + # Third-caster progression shared by Arcane Trickster (rogue) and Eldritch + # Knight (fighter): same slots, same Intelligence-based DCs, same prepared + # spell counts. Subclasses only name the class the spellcasting hangs off. + class ThirdCasterDecorator < ApplicationDecoratorV2 + SPELL_SLOTS = SpellSlots::THIRD_CASTER + + def call(result:) + @result = result + @result['spell_classes'][spellcasting_class] = spell_class_info + @result['spells_slots'] = SPELL_SLOTS[class_level] || SPELL_SLOTS[20] + @result + end + + private + + def spellcasting_class = raise(NotImplementedError) + + def spell_class_info + { + save_dc: 8 + proficiency_bonus + modifiers['int'], + attack_bonus: proficiency_bonus + modifiers['int'], + cantrips_amount: cantrips_amount, + max_spell_level: max_spell_level, + prepared_spells_amount: prepared_spells_amount, + multiclass_spell_level: class_level / 3 + } + end + + def class_level + @class_level ||= classes[spellcasting_class] + end + + def cantrips_amount + return 3 if class_level >= 10 + + 2 + end + + def prepared_spells_amount # rubocop: disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity + return 13 if class_level >= 20 + return 12 if class_level >= 19 + return 11 if class_level >= 16 + return 10 if class_level >= 14 + return 9 if class_level >= 13 + return 8 if class_level >= 11 + return 7 if class_level >= 10 + return 6 if class_level >= 8 + return 5 if class_level >= 7 + return 4 if class_level >= 4 + + 3 + end + + def max_spell_level + SPELL_SLOTS[class_level].keys.max + end + end + end +end diff --git a/app/javascript/applications/CharKeeperApp/components/atoms/Input.jsx b/app/javascript/applications/CharKeeperApp/components/atoms/Input.jsx index c12da84e3..8ad71f08a 100644 --- a/app/javascript/applications/CharKeeperApp/components/atoms/Input.jsx +++ b/app/javascript/applications/CharKeeperApp/components/atoms/Input.jsx @@ -1,4 +1,4 @@ -import { Switch, Match, splitProps } from 'solid-js'; +import { splitProps } from 'solid-js'; import { Label } from './Label'; @@ -12,56 +12,30 @@ export const Input = (props) => { props.onKeyDown(event); } + const type = () => { + if (props.numeric) return 'number'; + if (props.password) return 'password'; + + return 'text'; + } + return (