240 lines
7.9 KiB
Ruby
240 lines
7.9 KiB
Ruby
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<Pair<Color's
|
|
# human name (empty if standard), (Species => 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
|
|
|
|
# Generate hidden fields to preserve outfit state in URL params.
|
|
# Use the `except` parameter to skip certain fields, e.g. to override
|
|
# them with specific values, like in the species/color picker.
|
|
def outfit_state_params(outfit = @outfit, except: [])
|
|
fields = []
|
|
|
|
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
|
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
|
|
|
unless except.include?(:worn_items)
|
|
outfit.worn_items.each do |item|
|
|
fields << hidden_field_tag('objects[]', item.id)
|
|
end
|
|
end
|
|
|
|
unless except.include?(:q)
|
|
(params[:q] || {}).each do |key, value|
|
|
fields << hidden_field_tag("q[#{key}]", value) if value.present?
|
|
end
|
|
end
|
|
|
|
safe_join fields
|
|
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
|