Support zone restriction in new item preview
Adapted from wardrobe-2020's `getVisibleLayers`! Thanks past-Matchu for all the comments lol!
This commit is contained in:
parent
74748acaaf
commit
3f38fbd1b0
5 changed files with 91 additions and 25 deletions
|
@ -187,7 +187,7 @@ class ItemsController < ApplicationController
|
|||
appearance_params[:color_id], appearance_params[:species_id])
|
||||
end
|
||||
|
||||
target.appearances_for(@items.map(&:id), swf_asset_includes: [:zone]).
|
||||
target.appearances_for(@items, swf_asset_includes: [:zone]).
|
||||
tap do |appearances|
|
||||
# Preload the manifests for these SWF assets concurrently, rather than
|
||||
# loading them in sequence when we generate the JSON.
|
||||
|
@ -223,7 +223,7 @@ class ItemsController < ApplicationController
|
|||
def validate_preview
|
||||
if @selected_preview_pet_type.new_record?
|
||||
:pet_type_does_not_exist
|
||||
elsif @preview_outfit.item_appearances.values.any?(&:empty?)
|
||||
elsif @preview_outfit.item_appearances.any?(&:empty?)
|
||||
:no_item_data
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,9 +49,9 @@ class AltStyle < ApplicationRecord
|
|||
swf_asset.image_url
|
||||
end
|
||||
|
||||
# Given a list of item IDs, return how they look on this alt style.
|
||||
def appearances_for(item_ids, ...)
|
||||
Item.appearances_for(item_ids, self, ...)
|
||||
# Given a list of items, return how they look on this alt style.
|
||||
def appearances_for(items, ...)
|
||||
Item.appearances_for(items, self, ...)
|
||||
end
|
||||
|
||||
def biology=(biology)
|
||||
|
|
|
@ -555,17 +555,23 @@ class Item < ApplicationRecord
|
|||
# instead of like a hash, so you can target its children with things like
|
||||
# the `include` option. This feels clunky though, I wish I had something a
|
||||
# bit more suited to it!
|
||||
Appearance = Struct.new(:body, :swf_assets) do
|
||||
Appearance = Struct.new(:item, :body, :swf_assets) do
|
||||
include ActiveModel::Serializers::JSON
|
||||
delegate :present?, :empty?, to: :swf_assets
|
||||
|
||||
def attributes
|
||||
{body: body, swf_assets: swf_assets}
|
||||
{item:, body:, swf_assets:}
|
||||
end
|
||||
|
||||
def restricted_zone_ids
|
||||
return [] if empty?
|
||||
([item] + swf_assets).map(&:restricted_zone_ids).flatten.uniq.sort
|
||||
end
|
||||
end
|
||||
Appearance::Body = Struct.new(:id, :species) do
|
||||
include ActiveModel::Serializers::JSON
|
||||
def attributes
|
||||
{id: id, species: species}
|
||||
{id:, species:}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -582,7 +588,7 @@ class Item < ApplicationRecord
|
|||
# If there are no body-specific assets, return one appearance for them all.
|
||||
if swf_assets_by_body_id.empty?
|
||||
body = Appearance::Body.new(0, nil)
|
||||
return [Appearance.new(body, swf_assets_for_all_bodies)]
|
||||
return [Appearance.new(self, body, swf_assets_for_all_bodies)]
|
||||
end
|
||||
|
||||
# Otherwise, create an appearance for each real (nonzero) body ID. We don't
|
||||
|
@ -592,22 +598,22 @@ class Item < ApplicationRecord
|
|||
swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
|
||||
species = Species.with_body_id(body_id).first!
|
||||
body = Appearance::Body.new(body_id, species)
|
||||
Appearance.new(body, swf_assets_for_body)
|
||||
Appearance.new(self, body, swf_assets_for_body)
|
||||
end
|
||||
end
|
||||
|
||||
def appearance_for(target, ...)
|
||||
Item.appearances_for([id], target, ...)[id]
|
||||
Item.appearances_for([self], target, ...)[id]
|
||||
end
|
||||
|
||||
# Given a list of item IDs, return how they look on the given target (either
|
||||
# a pet type or an alt style).
|
||||
def self.appearances_for(item_ids, target, swf_asset_includes: [])
|
||||
# Given a list of items, return how they look on the given target (either a
|
||||
# pet type or an alt style).
|
||||
def self.appearances_for(items, target, swf_asset_includes: [])
|
||||
# First, load all the relationships for these items that also fit this
|
||||
# body.
|
||||
relationships = ParentSwfAssetRelationship.
|
||||
includes(swf_asset: swf_asset_includes).
|
||||
where(parent_type: "Item", parent_id: item_ids).
|
||||
where(parent_type: "Item", parent_id: items.map(&:id)).
|
||||
where(swf_asset: {body_id: [target.body_id, 0]})
|
||||
|
||||
pet_type_body = Appearance::Body.new(target.body_id, target.species)
|
||||
|
@ -618,13 +624,13 @@ class Item < ApplicationRecord
|
|||
transform_values { |rels| rels.map(&:swf_asset) }
|
||||
|
||||
# Finally, for each item, return an appearance—even if it's empty!
|
||||
item_ids.to_h do |item_id|
|
||||
assets = assets_by_item_id.fetch(item_id, [])
|
||||
items.to_h do |item|
|
||||
assets = assets_by_item_id.fetch(item.id, [])
|
||||
|
||||
fits_all_pets = assets.present? && assets.all? { |a| a.body_id == 0 }
|
||||
body = fits_all_pets ? all_pets_body : pet_type_body
|
||||
|
||||
[item_id, Appearance.new(body, assets)]
|
||||
[item.id, Appearance.new(item, body, assets)]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -160,13 +160,73 @@ class Outfit < ApplicationRecord
|
|||
end
|
||||
|
||||
def item_appearances(...)
|
||||
Item.appearances_for(worn_item_ids, pet_type, ...)
|
||||
Item.appearances_for(worn_items, pet_type, ...).values
|
||||
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
|
||||
item_appearances = item_appearances(swf_asset_includes: [:zone])
|
||||
|
||||
pet_layers = pet_state.swf_assets.includes(:zone).to_a
|
||||
item_layers = item_appearances.map(&:swf_assets).flatten
|
||||
|
||||
pet_restricted_zone_ids = pet_layers.map(&:restricted_zone_ids).
|
||||
flatten.to_set
|
||||
item_restricted_zone_ids = item_appearances.
|
||||
map(&:restricted_zone_ids).flatten.to_set
|
||||
|
||||
# When an item restricts a zone, it hides pet layers of the same zone.
|
||||
# We use this to e.g. make a hat hide a hair ruff.
|
||||
#
|
||||
# NOTE: Items' restricted layers also affect what items you can wear at
|
||||
# the same time. We don't enforce anything about that here, and
|
||||
# instead assume that the input by this point is valid!
|
||||
pet_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
# When a pet appearance restricts a zone, or when the pet is Unconverted,
|
||||
# it makes body-specific items incompatible. We use this to disallow UCs
|
||||
# from wearing certain body-specific Biology Effects, Statics, etc, while
|
||||
# still allowing non-body-specific items in those zones! (I think this
|
||||
# happens for some Invisible pet stuff, too?)
|
||||
#
|
||||
# TODO: We shouldn't be *hiding* these zones, like we do with items; we
|
||||
# should be doing this way earlier, to prevent the item from even
|
||||
# showing up even in search results!
|
||||
#
|
||||
# NOTE: This can result in both pet layers and items occupying the same
|
||||
# zone, like Static, so long as the item isn't body-specific! That's
|
||||
# correct, and the item layer should be on top! (Here, we implement
|
||||
# it by placing item layers second in the list, and rely on JS sort
|
||||
# stability, and *then* rely on the UI to respect that ordering when
|
||||
# rendering them by depth. Not great! 😅)
|
||||
#
|
||||
# NOTE: We used to also include the pet appearance's *occupied* zones in
|
||||
# this condition, not just the restricted zones, as a sensible
|
||||
# defensive default, even though we weren't aware of any relevant
|
||||
# items. But now we know that actually the "Bruce Brucey B Mouth"
|
||||
# occupies the real Mouth zone, and still should be visible and
|
||||
# above pet layers! So, we now only check *restricted* zones.
|
||||
#
|
||||
# NOTE: UCs used to implement their restrictions by listing specific
|
||||
# zones, but it seems that the logic has changed to just be about
|
||||
# UC-ness and body-specific-ness, and not necessarily involve the
|
||||
# set of restricted zones at all. (This matters because e.g. UCs
|
||||
# shouldn't show _any_ part of the Rainy Day Umbrella, but most UCs
|
||||
# don't restrict Right-Hand Item (Zone 49).) Still, I'm keeping the
|
||||
# zone restriction case running too, because I don't think it
|
||||
# _hurts_ anything, and I'm not confident enough in this conclusion.
|
||||
#
|
||||
# TODO: Do Invisibles follow this new rule like UCs, too? Or do they still
|
||||
# use zone restrictions?
|
||||
if pet_state.pose === "UNCONVERTED"
|
||||
item_layers.reject! { |sa| sa.body_specific? }
|
||||
else
|
||||
item_layers.reject! { |sa| sa.body_specific? &&
|
||||
pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
end
|
||||
|
||||
# A pet appearance can also restrict its own zones. The Wraith Uni is an
|
||||
# interesting example: it has a horn, but its zone restrictions hide it!
|
||||
pet_layers.reject! { |sa| pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
(pet_layers + item_layers).sort_by(&:depth)
|
||||
end
|
||||
|
|
|
@ -124,9 +124,9 @@ class PetType < ApplicationRecord
|
|||
}.first
|
||||
end
|
||||
|
||||
# Given a list of item IDs, return how they look on this pet type.
|
||||
def appearances_for(item_ids, ...)
|
||||
Item.appearances_for(item_ids, self, ...)
|
||||
# Given a list of items, return how they look on this pet type.
|
||||
def appearances_for(item, ...)
|
||||
Item.appearances_for(item, self, ...)
|
||||
end
|
||||
|
||||
def self.all_by_ids_or_children(ids, pet_states)
|
||||
|
|
Loading…
Reference in a new issue