# frozen_string_literal: true module Processors ## # SummonProcessor processes an array of summon data and creates GridSummon records. # It handles different summon types based on the +type+ parameter: # - :normal => standard summons # - :friend => friend summon (fixed position and uncap logic) # - :sub => sub summons (position based on order) # # @example # normal_processor = SummonProcessor.new(party, summons_array, :normal, quick_summon_id) # normal_processor.process # # friend_processor = SummonProcessor.new(party, [friend_summon_name], :friend) # friend_processor.process class SummonProcessor < BaseProcessor TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze ## # Initializes a new SummonProcessor. # # @param party [Party] the Party record. # @param data [Hash] the deck hash. # @param type [Symbol] the type of summon (:normal, :friend, or :sub). # @param quick_summon_id [String, nil] (optional) the quick summon identifier. # @param options [Hash] additional options. def initialize(party, data, type = :normal, options = {}) super(party, data, options) @party = party @data = data @type = type end ## # Processes summon data and creates GridSummon records. # # @return [void] def process unless @data.is_a?(Hash) Rails.logger.error "[SUMMON] Invalid data format: expected a Hash, got #{@data.class}" return end unless @data.key?('deck') && @data['deck'].key?('pc') Rails.logger.error '[SUMMON] Missing npc data in deck JSON' return end @data = @data.with_indifferent_access summons_data = @data.dig('deck', 'pc', 'summons') sub_summons_data = @data.dig('deck', 'pc', 'sub_summons') grid_summons = process_summons(summons_data, sub: false) friend_summon = process_friend_summon sub_summons = process_summons(sub_summons_data, sub: true) summons = [*grid_summons, friend_summon, *sub_summons] summons.each do |summon| summon.save! rescue ActiveRecord::RecordInvalid => e Rails.logger.error "[SUMMON] Failed to create GridSummon: #{e.record.errors.full_messages.join(', ')}" end end private attr_reader :type ## # Processes a set of summon data and creates GridSummon records. # # @param summons [Hash] the summon data # @param sub [Boolean] true if we are polling sub summons # @return [Array] def process_summons(summons, sub: false) internal_quick_summon_id = @data['quick_user_summon_id'].to_i if sub summons.map do |key, raw_summon| summon_params = raw_summon['param'] summon_id = raw_summon['master']['id'] summon = Summon.find_by(granblue_id: transform_id(summon_id)) position = if sub key.to_i + 4 else key.to_i == 1 ? -1 : key.to_i - 2 end GridSummon.new({ party: @party, summon: summon, position: position, main: key.to_i == 1, friend: false, quick_summon: summon_params['id'].to_i == internal_quick_summon_id, uncap_level: summon_params['evolution'].to_i, transcendence_step: level_to_transcendence(summon_params['level'].to_i), created_at: Time.now, updated_at: Time.now }) end end ## # Processes friend summon data and creates a GridSummon record. # # @return [GridSummon] def process_friend_summon summon_name = @data.dig('deck', 'pc', 'damage_info', 'summon_name') summon = Summon.find_by('name_en = ? OR name_jp = ?', summon_name, summon_name) GridSummon.new({ party: @party, summon: summon, position: 4, main: false, friend: true, quick_summon: false, uncap_level: determine_uncap_level(summon), transcendence_step: summon.transcendence ? 5 : 0, created_at: Time.now, updated_at: Time.now }) end ## # Determines the numeric uncap level of a given Summon # # @param summon [Summon] the canonical summon # @return [Integer] def determine_uncap_level(summon) if summon.transcendence 6 elsif summon.ulb 5 elsif summon.flb 4 else 3 end end ## # Determines the uncap level for a friend summon based on its ULb and FLb flags. # # @param summon_data [Hash] the summon data. # @return [Integer] the computed uncap level. def determine_friend_uncap(summon_data) if summon_data[:ulb] 5 elsif summon_data[:flb] 4 else 3 end end ## # Converts a given level, rounded down to the nearest 10, # to its corresponding transcendence step. # # If level is 200, returns 0; if level is 250, returns 5. # # @param level [Integer] the summon's level # @return [Integer] the transcendence step def level_to_transcendence(level) return 0 if level < 200 floored_level = (level / 10).floor * 10 TRANSCENDENCE_LEVELS.index(floored_level) end ## # Transforms 5★ Arcarum-series summon IDs into their 4★ variants, # as that's what is stored in the database. # # If an unrelated ID, or the 4★ ID is passed, then returns the input. # # @param id [String] the ID to match # @return [String] the resulting ID def transform_id(id) mapping = { '2040315000' => '2040238000', '2040316000' => '2040239000', '2040314000' => '2040237000', '2040313000' => '2040236000', '2040321000' => '2040244000', '2040319000' => '2040242000', '2040317000' => '2040240000', '2040322000' => '2040245000', '2040318000' => '2040241000', '2040320000' => '2040243000' } # If the id is a key, return the mapped value; otherwise, return the id. mapping[id] || id end end end