class Outfit < ApplicationRecord has_many :item_outfit_relationships, :dependent => :destroy has_many :worn_item_outfit_relationships, -> { where(is_worn: true) }, class_name: 'ItemOutfitRelationship' has_many :worn_items, through: :worn_item_outfit_relationships, source: :item belongs_to :alt_style, optional: true belongs_to :pet_state, optional: true # We validate presence below! belongs_to :user, optional: true validates :name, :presence => {:if => :user_id}, :uniqueness => {:scope => :user_id, :if => :user_id} validates :pet_state, presence: { message: ->(object, _) do if object.biology "does not exist for " + "species ##{object.biology[:species_id]}, " + "color ##{object.biology[:color_id]}, " + "pose #{object.biology[:pose]}" else "must exist" end end } before_validation :ensure_unique_name, if: :user_id? attr_reader :biology delegate :pose, to: :pet_state delegate :pet_type, to: :pet_state delegate :color, to: :pet_type delegate :color_id, to: :pet_type delegate :species, to: :pet_type delegate :species_id, to: :pet_type scope :wardrobe_order, -> { order('starred DESC', :name) } class OutfitImage def initialize(image_versions) @image_versions = image_versions end def url @image_versions[:large] end def large Version.new(@image_versions[:large]) end def medium Version.new(@image_versions[:medium]) end def small Version.new(@image_versions[:small]) end Version = Struct.new(:url) end def image? true end def image OutfitImage.new(image_versions) end IMAGE_URL_TEMPLATE = Addressable::Template.new( Rails.configuration.impress_2020_origin + "/api/outfitImage{?id,size,updatedAt}" ) def image_versions if Rails.env.production? # Now, instead of using the saved outfit to S3, we're using out the # DTI 2020 API + CDN cache version. We use openneo-assets.net to get # around a bug on Neopets petpages with openneo.net URLs. base_url = "https://outfits.openneo-assets.net/outfits" + "/#{CGI.escape id.to_s}" + "/v/#{CGI.escape updated_at.to_i.to_s}" { large: "#{base_url}/600.png", medium: "#{base_url}/300.png", small: "#{base_url}/150.png", } else # In development, just talk to our local Impress 2020 directly. build_url = -> size { IMAGE_URL_TEMPLATE.expand( id: id, size: size, updatedAt: updated_at.to_i ).to_s } { large: build_url.call("600"), medium: build_url.call("300"), small: build_url.call("150"), } end # NOTE: Below is the previous code that uses the saved outfits! # {}.tap do |versions| # versions[:large] = image.url # image.versions.each { |name, version| versions[name] = version.url } # end end def as_json(more_options={}) serializable_hash( only: [:id, :name, :pet_state_id, :starred, :created_at, :updated_at, :alt_style_id], methods: [:color_id, :species_id, :pose, :item_ids, :user] ) end def biology=(biology) @biology = biology.slice(:species_id, :color_id, :pose, :pet_state_id) begin if @biology[:pet_state_id] self.pet_state = PetState.find(@biology[:pet_state_id]) else pet_type = PetType.where( species_id: @biology[:species_id], color_id: @biology[:color_id], ).first! self.pet_state = pet_type.pet_states.with_pose(@biology[:pose]). emotion_order.first! end rescue ActiveRecord::RecordNotFound # If there's no such pet state (which shouldn't happen normally in-app), # we don't set `pet_state` but we keep `@biology` for validation. end end def item_ids rels = item_outfit_relationships { worn: rels.filter { |r| r.is_worn? }.map { |r| r.item_id }, closeted: rels.filter { |r| !r.is_worn? }.map { |r| r.item_id } } end def item_ids=(item_ids) # Ensure there are no duplicates between the worn/closeted IDs. If an ID is # present in both, it's kept in `worn` and removed from `closeted`. worn_item_ids = item_ids.fetch(:worn, []).uniq closeted_item_ids = item_ids.fetch(:closeted, []).uniq closeted_item_ids.reject! { |id| worn_item_ids.include?(id) } # Set the worn and closeted item outfit relationships. If there are any # others attached to this outfit, they are implicitly deleted. new_relationships = [] new_relationships += worn_item_ids.map do |item_id| ItemOutfitRelationship.new(item_id: item_id, is_worn: true) end new_relationships += closeted_item_ids.map do |item_id| ItemOutfitRelationship.new(item_id: item_id, is_worn: false) end self.item_outfit_relationships = new_relationships end def item_appearances(...) Item.appearances_for(worn_item_ids, pet_type, ...) end def visible_layers pet_layers = pet_state.swf_assets.includes(:zone) item_layers = item_appearances(swf_asset_includes: [:zone]).values. map(&:swf_assets).flatten (pet_layers + item_layers).sort_by(&:depth) end def ensure_unique_name # If no name was provided, start with "Untitled outfit". self.name = "Untitled outfit" if name.blank? # Strip whitespace from the name. self.name.strip! # Get the base name of the provided name, without any "(1)" suffixes. base_name = name.sub(/\s*\([0-9]+\)\z/, '') # Find the user's other outfits that start with the same base name, and get # *their* names, with whitespace stripped. existing_outfits = self.user.outfits. where("name LIKE ?", Outfit.sanitize_sql_like(base_name) + "%") existing_outfits = existing_outfits.where("id != ?", id) unless id.nil? existing_names = existing_outfits.map(&:name).map(&:strip) # Try the provided name first, but if it's taken, add a "(1)" suffix and # keep incrementing it until it's not. i = 1 while existing_names.include?(name) self.name = "#{base_name} (#{i})" i += 1 end end end