module OutfitsHelper def destination_tag(value) hidden_field_tag 'destination', value, :id => nil end def latest_contribution_description(contribution) user = contribution.user contributed = contribution.contributed t 'outfits.new.latest_contribution.description_html', :user_link => link_to(user.name, user_contributions_path(user)), :contributed_description => contributed_description(contributed, false) end def render_predicted_missing_species_by_color(species_by_color) key_prefix = 'outfits.new.newest_items.unmodeled.content' # Transform the Color => (Species => Int) map into an Array Int)>>. standard = species_by_color.delete(:standard) sorted_pairs = species_by_color.to_a.map { |k, v| [k.human_name, v] }. sort_by { |k, v| k } sorted_pairs.unshift(['', standard]) if standard species_by_color[:standard] = standard # undo parameter mutation first = true contents = sorted_pairs.map { |color_human_name, body_ids_by_species| species_list = body_ids_by_species.keys.sort_by(&:human_name).map { |species| body_id = body_ids_by_species[species] content_tag(:span, species.human_name, 'data-body-id' => body_id) }.to_sentence( words_connector: t("#{key_prefix}.species_list.words_connector"), two_words_connector: t("#{key_prefix}.species_list.two_words_connector"), last_word_connector: t("#{key_prefix}.species_list.last_word_connector") ) key = first ? 'first' : 'other' content = t("#{key_prefix}.body.#{key}", color: color_human_name, species_list: species_list).html_safe first = false content } contents.last << " " + t("#{key_prefix}.call_to_action") content_tags = contents.map { |c| content_tag(:p, c) } content_tags.join('').html_safe end def outfit_li_for(outfit, &block) class_name = outfit.starred? ? 'starred' : nil content_tag :li, :class => class_name, &block end def outfit_image_tag(outfit) image_tag( outfit.image.small.url, srcset: [[outfit.image.medium.url, "2x"]], ) end def pet_attribute_select(name, collection, value=nil) options = options_from_collection_for_select(collection, :id, :human_name, value) select_tag name, options, id: nil, class: name end def pet_name_tag(options={}) options = {:spellcheck => false, :id => nil}.merge(options) text_field_tag 'name', nil, options end def outfit_viewer(...) render partial: "outfit_viewer", locals: parse_outfit_viewer_options(...) end def support_outfit_viewer(...) render partial: "support_outfit_viewer", locals: parse_outfit_viewer_options(...) end # Group outfit items by zone, applying smart multi-zone simplification. # Returns an array of hashes: {zone:, items:} # This matches the logic from wardrobe-2020's getZonesAndItems function. def outfit_items_by_zone(outfit) return [] if outfit.pet_type.nil? # Get item appearances for this outfit item_appearances = Item.appearances_for( outfit.worn_items, outfit.pet_type, swf_asset_includes: [:zone] ) # Separate incompatible items (no layers for this pet) compatible_items = [] incompatible_items = [] outfit.worn_items.each do |item| appearance = item_appearances[item.id] if appearance&.present? compatible_items << {item: item, appearance: appearance} else incompatible_items << item end end # Group items by zone - multi-zone items appear in each zone items_by_zone = Hash.new { |h, k| h[k] = [] } zones_by_id = {} compatible_items.each do |item_with_appearance| item = item_with_appearance[:item] appearance = item_with_appearance[:appearance] # Get unique zones for this item (an item may have multiple assets per zone) appearance.swf_assets.map(&:zone).uniq.each do |zone| zones_by_id[zone.id] = zone items_by_zone[zone.id] << item end end # Create zone groups with sorted items zones_and_items = items_by_zone.map do |zone_id, items| { zone_id: zone_id, zone_label: zones_by_id[zone_id].label, items: items.sort_by { |item| item.name.downcase } } end # Sort zone groups alphabetically by label, then by ID for tiebreaking zones_and_items.sort_by! do |group| [group[:zone_label].downcase, group[:zone_id]] end # Apply multi-zone simplification: remove redundant single-item groups zones_and_items = simplify_multi_zone_groups(zones_and_items) # Add zone ID disambiguation for duplicate labels zones_and_items = disambiguate_zone_labels(zones_and_items) # Add incompatible items section if any if incompatible_items.any? zones_and_items << { zone_id: nil, zone_label: "Incompatible", items: incompatible_items.sort_by { |item| item.name.downcase } } end zones_and_items end private # Simplify zone groups by removing redundant single-item groups. # Keep groups with multiple items (conflicts). For single-item groups, # only keep them if the item doesn't appear in a multi-item group. def simplify_multi_zone_groups(zones_and_items) # Find groups with conflicts (multiple items) groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 } # Track which items appear in conflict groups items_with_conflicts = Set.new( groups_with_conflicts.flat_map { |g| g[:items].map(&:id) } ) # Track which items we've already shown items_we_have_seen = Set.new # Filter groups zones_and_items.select do |group| # Always keep groups with multiple items if group[:items].length > 1 group[:items].each { |item| items_we_have_seen.add(item.id) } true else # For single-item groups, only keep if: # - Item hasn't been seen yet AND # - Item won't appear in a conflict group item = group[:items].first item_id = item.id if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id) false else items_we_have_seen.add(item_id) true end end end end # Add zone IDs to labels when there are duplicates def disambiguate_zone_labels(zones_and_items) label_counts = zones_and_items.group_by { |g| g[:zone_label] } .transform_values(&:count) zones_and_items.each do |group| if label_counts[group[:zone_label]] > 1 group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})" end end zones_and_items end def parse_outfit_viewer_options( outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options ) outfit = Outfit.new(pet_state:) if outfit.nil? && pet_state.present? if outfit.nil? raise ArgumentError, "outfit viewer must have outfit or pet state" end {outfit:, preferred_image_format:, html_options:} end end