Compare commits
6 commits
054c809052
...
0a9193aed7
Author | SHA1 | Date | |
---|---|---|---|
0a9193aed7 | |||
ac002f3151 | |||
fe6035d438 | |||
21a8a49f50 | |||
3f38fbd1b0 | |||
74748acaaf |
10 changed files with 180 additions and 58 deletions
|
@ -38,7 +38,7 @@ body.items-show
|
|||
height: 16px
|
||||
width: 16px
|
||||
|
||||
outfit-viewer
|
||||
.outfit-viewer
|
||||
position: relative
|
||||
display: block
|
||||
width: 300px
|
||||
|
@ -49,7 +49,7 @@ body.items-show
|
|||
|
||||
margin: 0 auto .75em
|
||||
|
||||
outfit-layer
|
||||
.outfit-layer
|
||||
display: block
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
@ -58,6 +58,9 @@ body.items-show
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
&:has(.outfit-layer[data-status="loading"])
|
||||
background: gray
|
||||
|
||||
.species-color-picker
|
||||
.error-icon
|
||||
cursor: help
|
||||
|
|
|
@ -84,14 +84,10 @@ class ItemsController < ApplicationController
|
|||
end
|
||||
|
||||
@selected_preview_pet_type = load_selected_preview_pet_type
|
||||
@preview_pet_type = load_preview_pet_type
|
||||
|
||||
@item_layers = @item.appearance_for(
|
||||
@preview_pet_type, swf_asset_includes: [:zone]
|
||||
).swf_assets
|
||||
@pet_layers = @preview_pet_type.canonical_pet_state.swf_assets.
|
||||
includes(:zone)
|
||||
|
||||
@preview_outfit = Outfit.new(
|
||||
pet_state: load_preview_pet_type.canonical_pet_state,
|
||||
worn_items: [@item],
|
||||
)
|
||||
@preview_error = validate_preview
|
||||
end
|
||||
|
||||
|
@ -191,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.
|
||||
|
@ -206,7 +202,12 @@ class ItemsController < ApplicationController
|
|||
|
||||
return load_default_preview_pet_type if color_id.nil? || species_id.nil?
|
||||
|
||||
PetType.find_or_initialize_by(color_id:, species_id:)
|
||||
PetType.find_or_initialize_by(color_id:, species_id:).tap do |pet_type|
|
||||
if pet_type.persisted?
|
||||
cookies["preferred-preview-color-id"] = color_id
|
||||
cookies["preferred-preview-species-id"] = species_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_preview_pet_type
|
||||
|
@ -218,16 +219,16 @@ class ItemsController < ApplicationController
|
|||
end
|
||||
|
||||
def load_default_preview_pet_type
|
||||
PetType.find_by_color_id_and_species_id(
|
||||
Color.find_by_name("Blue"),
|
||||
Species.find_by_name("Acara"),
|
||||
)
|
||||
@item.compatible_pet_types.
|
||||
preferring_species(cookies["preferred-preview-species-id"] || "<ignore>").
|
||||
preferring_color(cookies["preferred-preview-color-id"] || "<ignore>").
|
||||
preferring_simple.first
|
||||
end
|
||||
|
||||
def validate_preview
|
||||
if @selected_preview_pet_type.new_record?
|
||||
:pet_type_does_not_exist
|
||||
elsif @item_layers.empty?
|
||||
elsif @preview_outfit.item_appearances.any?(&:empty?)
|
||||
:no_item_data
|
||||
end
|
||||
end
|
||||
|
|
|
@ -250,7 +250,7 @@ module ItemsHelper
|
|||
end
|
||||
|
||||
def outfit_viewer_layer(swf_asset)
|
||||
content_tag "outfit-layer", style: "z-index: #{swf_asset.zone.depth}" do
|
||||
content_tag :div, class: "outfit-layer" do
|
||||
image_tag swf_asset.image_url, alt: ""
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,32 @@
|
|||
// eslint-disable-next-line no-console
|
||||
console.log("OwO!");
|
||||
// When we load in a new preview, set the status on the images. We use this in
|
||||
// our CSS to show a loading state when needed.
|
||||
function setImageStatuses() {
|
||||
for (const layer of document.querySelectorAll(".outfit-layer")) {
|
||||
const isLoaded = layer.querySelector("img").complete;
|
||||
layer.setAttribute("data-status", isLoaded ? "loaded" : "loading");
|
||||
}
|
||||
}
|
||||
document.addEventListener("turbo:frame-render", () => setImageStatuses());
|
||||
document.addEventListener("turbo:load", () => setImageStatuses());
|
||||
|
||||
// When a preview image loads or fails, update its status. (Note that `load`
|
||||
// does not fire for images that were loaded from cache, which is why we need
|
||||
// both this and `setImageStatuses` when rendering new images!)
|
||||
document.addEventListener(
|
||||
"load",
|
||||
({ target }) => {
|
||||
if (target.matches(".outfit-layer img")) {
|
||||
target.closest(".outfit-layer").setAttribute("data-status", "loaded");
|
||||
}
|
||||
},
|
||||
{ capture: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
"error",
|
||||
({ target }) => {
|
||||
if (target.matches(".outfit-layer img")) {
|
||||
target.closest(".outfit-layer").setAttribute("data-status", "error");
|
||||
}
|
||||
},
|
||||
{ capture: true },
|
||||
);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -489,6 +489,15 @@ class Item < ApplicationRecord
|
|||
}.merge(options))
|
||||
end
|
||||
|
||||
def compatible_body_ids
|
||||
swf_assets.map(&:body_id).uniq
|
||||
end
|
||||
|
||||
def compatible_pet_types
|
||||
return PetType.all if compatible_body_ids.include?(0)
|
||||
PetType.where(body_id: compatible_body_ids)
|
||||
end
|
||||
|
||||
def handle_assets!
|
||||
if @parent_swf_asset_relationships_to_update && @current_body_id
|
||||
new_swf_asset_ids = @parent_swf_asset_relationships_to_update.map(&:swf_asset_id)
|
||||
|
@ -555,16 +564,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
|
||||
|
||||
|
@ -581,7 +597,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
|
||||
|
@ -591,22 +607,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)
|
||||
|
@ -617,13 +633,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
|
||||
|
||||
|
|
|
@ -25,7 +25,12 @@ class Outfit < ApplicationRecord
|
|||
before_validation :ensure_unique_name, if: :user_id?
|
||||
|
||||
attr_reader :biology
|
||||
delegate :color, to: :pet_state
|
||||
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) }
|
||||
|
||||
|
@ -107,18 +112,6 @@ class Outfit < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
def color_id
|
||||
pet_state.pet_type.color_id
|
||||
end
|
||||
|
||||
def species_id
|
||||
pet_state.pet_type.species_id
|
||||
end
|
||||
|
||||
def pose
|
||||
pet_state.pose
|
||||
end
|
||||
|
||||
def biology=(biology)
|
||||
@biology = biology.slice(:species_id, :color_id, :pose, :pet_state_id)
|
||||
|
||||
|
@ -166,6 +159,78 @@ class Outfit < ApplicationRecord
|
|||
self.item_outfit_relationships = new_relationships
|
||||
end
|
||||
|
||||
def item_appearances(...)
|
||||
Item.appearances_for(worn_items, pet_type, ...).values
|
||||
end
|
||||
|
||||
def visible_layers
|
||||
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
|
||||
|
||||
def ensure_unique_name
|
||||
# If no name was provided, start with "Untitled outfit".
|
||||
self.name = "Untitled outfit" if name.blank?
|
||||
|
|
|
@ -16,6 +16,17 @@ class PetType < ApplicationRecord
|
|||
species = Species.find_by_name!(species_name)
|
||||
where(color_id: color.id, species_id: species.id)
|
||||
}
|
||||
scope :preferring_species, ->(species_id) {
|
||||
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
|
||||
}
|
||||
scope :preferring_color, ->(color_id) {
|
||||
joins(:color).order([Arel.sql("color_id = ? DESC"), color_id])
|
||||
}
|
||||
scope :preferring_simple, -> {
|
||||
joins(:species, :color).
|
||||
merge(Species.order(name: :asc)).
|
||||
merge(Color.order(basic: :desc, standard: :desc, name: :asc))
|
||||
}
|
||||
|
||||
def self.special_color_or_basic(special_color)
|
||||
color_ids = special_color ? [special_color.id] : Color.basic.select([:id]).map(&:id)
|
||||
|
@ -124,9 +135,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)
|
||||
|
|
|
@ -15,7 +15,6 @@ class SwfAsset < ApplicationRecord
|
|||
belongs_to :zone
|
||||
has_many :parent_swf_asset_relationships
|
||||
has_one :contribution, :as => :contributed, :inverse_of => :contributed
|
||||
has_many :parent_swf_asset_relationships
|
||||
|
||||
before_validation :normalize_manifest_url, if: :manifest_url?
|
||||
|
||||
|
|
|
@ -14,11 +14,8 @@
|
|||
sorry!
|
||||
|
||||
= turbo_frame_tag "item-preview" do
|
||||
%outfit-viewer
|
||||
%outfit-pet-appearance
|
||||
= outfit_viewer_layers @pet_layers
|
||||
%outfit-item-appearance
|
||||
= outfit_viewer_layers @item_layers
|
||||
.outfit-viewer
|
||||
= outfit_viewer_layers @preview_outfit.visible_layers
|
||||
|
||||
= form_for item_path(@item), method: :get, class: "species-color-picker",
|
||||
data: {"is-valid": @preview_error.nil?} do |f|
|
||||
|
|
Loading…
Reference in a new issue