From e8d768961b579ef8abf2d73056c92f022bf8b63e Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Mon, 3 Nov 2025 00:07:08 +0000 Subject: [PATCH] [WV2] Group items by zone --- app/helpers/outfits_helper.rb | 125 +++++++++++++++++++++++++++++ app/views/outfits/new_v2.html.haml | 24 +++--- 2 files changed, 139 insertions(+), 10 deletions(-) diff --git a/app/helpers/outfits_helper.rb b/app/helpers/outfits_helper.rb index a31d8587..498ca9a2 100644 --- a/app/helpers/outfits_helper.rb +++ b/app/helpers/outfits_helper.rb @@ -75,8 +75,133 @@ module OutfitsHelper 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 ) diff --git a/app/views/outfits/new_v2.html.haml b/app/views/outfits/new_v2.html.haml index bb3fb5fb..b26fdbe8 100644 --- a/app/views/outfits/new_v2.html.haml +++ b/app/views/outfits/new_v2.html.haml @@ -51,13 +51,17 @@ - if @outfit.worn_items.any? .worn-items %h2 Items (#{@outfit.worn_items.size}) - %ul.items-list - - @outfit.worn_items.each do |item| - %li.item-card - .item-thumbnail - = image_tag item.thumbnail_url, alt: item.name, loading: "lazy" - .item-info - .item-name= item.name - .item-badges - = render "items/badges/kind", item: item - = render "items/badges/first_seen", item: item + + - outfit_items_by_zone(@outfit).each do |zone_group| + .zone-group + %h3.zone-label= zone_group[:zone_label] + %ul.items-list + - zone_group[:items].each do |item| + %li.item-card + .item-thumbnail + = image_tag item.thumbnail_url, alt: item.name, loading: "lazy" + .item-info + .item-name= item.name + .item-badges + = render "items/badges/kind", item: item + = render "items/badges/first_seen", item: item