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])
|
appearance_params[:color_id], appearance_params[:species_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
target.appearances_for(@items.map(&:id), swf_asset_includes: [:zone]).
|
target.appearances_for(@items, swf_asset_includes: [:zone]).
|
||||||
tap do |appearances|
|
tap do |appearances|
|
||||||
# Preload the manifests for these SWF assets concurrently, rather than
|
# Preload the manifests for these SWF assets concurrently, rather than
|
||||||
# loading them in sequence when we generate the JSON.
|
# loading them in sequence when we generate the JSON.
|
||||||
|
@ -223,7 +223,7 @@ class ItemsController < ApplicationController
|
||||||
def validate_preview
|
def validate_preview
|
||||||
if @selected_preview_pet_type.new_record?
|
if @selected_preview_pet_type.new_record?
|
||||||
:pet_type_does_not_exist
|
:pet_type_does_not_exist
|
||||||
elsif @preview_outfit.item_appearances.values.any?(&:empty?)
|
elsif @preview_outfit.item_appearances.any?(&:empty?)
|
||||||
:no_item_data
|
:no_item_data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,9 +49,9 @@ class AltStyle < ApplicationRecord
|
||||||
swf_asset.image_url
|
swf_asset.image_url
|
||||||
end
|
end
|
||||||
|
|
||||||
# Given a list of item IDs, return how they look on this alt style.
|
# Given a list of items, return how they look on this alt style.
|
||||||
def appearances_for(item_ids, ...)
|
def appearances_for(items, ...)
|
||||||
Item.appearances_for(item_ids, self, ...)
|
Item.appearances_for(items, self, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
def biology=(biology)
|
def biology=(biology)
|
||||||
|
|
|
@ -555,17 +555,23 @@ class Item < ApplicationRecord
|
||||||
# instead of like a hash, so you can target its children with things like
|
# 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
|
# the `include` option. This feels clunky though, I wish I had something a
|
||||||
# bit more suited to it!
|
# bit more suited to it!
|
||||||
Appearance = Struct.new(:body, :swf_assets) do
|
Appearance = Struct.new(:item, :body, :swf_assets) do
|
||||||
include ActiveModel::Serializers::JSON
|
include ActiveModel::Serializers::JSON
|
||||||
delegate :present?, :empty?, to: :swf_assets
|
delegate :present?, :empty?, to: :swf_assets
|
||||||
|
|
||||||
def attributes
|
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
|
||||||
end
|
end
|
||||||
Appearance::Body = Struct.new(:id, :species) do
|
Appearance::Body = Struct.new(:id, :species) do
|
||||||
include ActiveModel::Serializers::JSON
|
include ActiveModel::Serializers::JSON
|
||||||
def attributes
|
def attributes
|
||||||
{id: id, species: species}
|
{id:, species:}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -582,7 +588,7 @@ class Item < ApplicationRecord
|
||||||
# If there are no body-specific assets, return one appearance for them all.
|
# If there are no body-specific assets, return one appearance for them all.
|
||||||
if swf_assets_by_body_id.empty?
|
if swf_assets_by_body_id.empty?
|
||||||
body = Appearance::Body.new(0, nil)
|
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
|
end
|
||||||
|
|
||||||
# Otherwise, create an appearance for each real (nonzero) body ID. We don't
|
# 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
|
swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
|
||||||
species = Species.with_body_id(body_id).first!
|
species = Species.with_body_id(body_id).first!
|
||||||
body = Appearance::Body.new(body_id, species)
|
body = Appearance::Body.new(body_id, species)
|
||||||
Appearance.new(body, swf_assets_for_body)
|
Appearance.new(self, body, swf_assets_for_body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def appearance_for(target, ...)
|
def appearance_for(target, ...)
|
||||||
Item.appearances_for([id], target, ...)[id]
|
Item.appearances_for([self], target, ...)[id]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Given a list of item IDs, return how they look on the given target (either
|
# Given a list of items, return how they look on the given target (either a
|
||||||
# a pet type or an alt style).
|
# pet type or an alt style).
|
||||||
def self.appearances_for(item_ids, target, swf_asset_includes: [])
|
def self.appearances_for(items, target, swf_asset_includes: [])
|
||||||
# First, load all the relationships for these items that also fit this
|
# First, load all the relationships for these items that also fit this
|
||||||
# body.
|
# body.
|
||||||
relationships = ParentSwfAssetRelationship.
|
relationships = ParentSwfAssetRelationship.
|
||||||
includes(swf_asset: swf_asset_includes).
|
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]})
|
where(swf_asset: {body_id: [target.body_id, 0]})
|
||||||
|
|
||||||
pet_type_body = Appearance::Body.new(target.body_id, target.species)
|
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) }
|
transform_values { |rels| rels.map(&:swf_asset) }
|
||||||
|
|
||||||
# Finally, for each item, return an appearance—even if it's empty!
|
# Finally, for each item, return an appearance—even if it's empty!
|
||||||
item_ids.to_h do |item_id|
|
items.to_h do |item|
|
||||||
assets = assets_by_item_id.fetch(item_id, [])
|
assets = assets_by_item_id.fetch(item.id, [])
|
||||||
|
|
||||||
fits_all_pets = assets.present? && assets.all? { |a| a.body_id == 0 }
|
fits_all_pets = assets.present? && assets.all? { |a| a.body_id == 0 }
|
||||||
body = fits_all_pets ? all_pets_body : pet_type_body
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -160,13 +160,73 @@ class Outfit < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def item_appearances(...)
|
def item_appearances(...)
|
||||||
Item.appearances_for(worn_item_ids, pet_type, ...)
|
Item.appearances_for(worn_items, pet_type, ...).values
|
||||||
end
|
end
|
||||||
|
|
||||||
def visible_layers
|
def visible_layers
|
||||||
pet_layers = pet_state.swf_assets.includes(:zone)
|
item_appearances = item_appearances(swf_asset_includes: [:zone])
|
||||||
item_layers = item_appearances(swf_asset_includes: [:zone]).values.
|
|
||||||
map(&:swf_assets).flatten
|
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)
|
(pet_layers + item_layers).sort_by(&:depth)
|
||||||
end
|
end
|
||||||
|
|
|
@ -124,9 +124,9 @@ class PetType < ApplicationRecord
|
||||||
}.first
|
}.first
|
||||||
end
|
end
|
||||||
|
|
||||||
# Given a list of item IDs, return how they look on this pet type.
|
# Given a list of items, return how they look on this pet type.
|
||||||
def appearances_for(item_ids, ...)
|
def appearances_for(item, ...)
|
||||||
Item.appearances_for(item_ids, self, ...)
|
Item.appearances_for(item, self, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.all_by_ids_or_children(ids, pet_states)
|
def self.all_by_ids_or_children(ids, pet_states)
|
||||||
|
|
Loading…
Reference in a new issue