Display alt styles in outfit editor when selected

Yay it works(*)! But two major missing pieces:

- Outfit saving doesn't persist it at all
- Item compatibility is unaffected: items will still appear in search
  and in the preview, even when they don't fit anymore.
This commit is contained in:
Emi Matchu 2024-01-30 07:01:03 -08:00
parent 3ebbfc4967
commit c2de6f7167
7 changed files with 65 additions and 8 deletions

View file

@ -11,7 +11,8 @@ class AltStylesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html { render } format.html { render }
format.json { format.json {
render json: @alt_styles.as_json( render json: @alt_styles.includes(swf_assets: [:zone]).as_json(
include: {swf_assets: {include: [:zone], methods: [:image_url]}},
methods: [:series_name, :adjective_name, :thumbnail_url], methods: [:series_name, :adjective_name, :thumbnail_url],
) )
} }

View file

@ -24,6 +24,7 @@ function WardrobePreviewAndControls({
speciesId: outfitState.speciesId, speciesId: outfitState.speciesId,
colorId: outfitState.colorId, colorId: outfitState.colorId,
pose: outfitState.pose, pose: outfitState.pose,
altStyleId: outfitState.altStyleId,
appearanceId: outfitState.appearanceId, appearanceId: outfitState.appearanceId,
wornItemIds: outfitState.wornItemIds, wornItemIds: outfitState.wornItemIds,
onChangeHasAnimations: setHasAnimations, onChangeHasAnimations: setHasAnimations,

View file

@ -413,11 +413,12 @@ function ItemSupportPetCompatibilityRuleFields({
*/ */
function ItemSupportAppearanceLayers({ item }) { function ItemSupportAppearanceLayers({ item }) {
const outfitState = React.useContext(OutfitStateContext); const outfitState = React.useContext(OutfitStateContext);
const { speciesId, colorId, pose, appearanceId } = outfitState; const { speciesId, colorId, pose, altStyleId, appearanceId } = outfitState;
const { error, visibleLayers } = useOutfitAppearance({ const { error, visibleLayers } = useOutfitAppearance({
speciesId, speciesId,
colorId, colorId,
pose, pose,
altStyleId,
appearanceId, appearanceId,
wornItemIds: [item.id], wornItemIds: [item.id],
}); });

View file

@ -52,6 +52,7 @@ export function useOutfitPreview({
speciesId, speciesId,
colorId, colorId,
pose, pose,
altStyleId,
wornItemIds, wornItemIds,
appearanceId = null, appearanceId = null,
isLoading = false, isLoading = false,
@ -68,6 +69,7 @@ export function useOutfitPreview({
speciesId, speciesId,
colorId, colorId,
pose, pose,
altStyleId,
appearanceId, appearanceId,
wornItemIds, wornItemIds,
}); });

View file

@ -1,17 +1,20 @@
import React from "react"; import React from "react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import getVisibleLayers, { import getVisibleLayers, {
itemAppearanceFragmentForGetVisibleLayers, itemAppearanceFragmentForGetVisibleLayers,
petAppearanceFragmentForGetVisibleLayers, petAppearanceFragmentForGetVisibleLayers,
} from "../components/getVisibleLayers"; } from "./getVisibleLayers";
import { useAltStyle } from "../loaders/alt-styles";
/** /**
* useOutfitAppearance downloads the outfit's appearance data, and returns * useOutfitAppearance downloads the outfit's appearance data, and returns
* visibleLayers for rendering. * visibleLayers for rendering.
*/ */
export default function useOutfitAppearance(outfitState) { export default function useOutfitAppearance(outfitState) {
const { wornItemIds, speciesId, colorId, pose, appearanceId } = outfitState; const { wornItemIds, speciesId, colorId, pose, altStyleId, appearanceId } =
outfitState;
// We split this query out from the other one, so that we can HTTP cache it. // We split this query out from the other one, so that we can HTTP cache it.
// //
@ -102,7 +105,13 @@ export default function useOutfitAppearance(outfitState) {
}, },
); );
const petAppearance = data1?.petAppearance; const {
isLoading: loading3,
error: error3,
data: altStyle,
} = useAltStyle(altStyleId, speciesId);
const petAppearance = altStyle?.appearance ?? data1?.petAppearance;
const items = data2?.items; const items = data2?.items;
const itemAppearances = React.useMemo( const itemAppearances = React.useMemo(
() => (items || []).map((i) => i.appearance), () => (items || []).map((i) => i.appearance),
@ -116,8 +125,8 @@ export default function useOutfitAppearance(outfitState) {
const bodyId = petAppearance?.bodyId; const bodyId = petAppearance?.bodyId;
return { return {
loading: loading1 || loading2, loading: loading1 || loading2 || loading3,
error: error1 || error2, error: error1 || error2 || error3,
petAppearance, petAppearance,
items: items || [], items: items || [],
itemAppearances, itemAppearances,

View file

@ -8,6 +8,17 @@ export function useAltStylesForSpecies(speciesId, options = {}) {
}); });
} }
// NOTE: This is actually just a wrapper for `useAltStylesForSpecies`, to share
// the same cache key!
export function useAltStyle(id, speciesId, options = {}) {
const query = useAltStylesForSpecies(speciesId, options);
return {
...query,
data: query.data?.find((s) => s.id === id) ?? null,
};
}
async function loadAltStylesForSpecies(speciesId) { async function loadAltStylesForSpecies(speciesId) {
const res = await fetch( const res = await fetch(
`/species/${encodeURIComponent(speciesId)}/alt-styles.json`, `/species/${encodeURIComponent(speciesId)}/alt-styles.json`,
@ -35,5 +46,37 @@ function normalizeAltStyle(altStyleData) {
seriesName: altStyleData.series_name, seriesName: altStyleData.series_name,
adjectiveName: altStyleData.adjective_name, adjectiveName: altStyleData.adjective_name,
thumbnailUrl: altStyleData.thumbnail_url, thumbnailUrl: altStyleData.thumbnail_url,
// This matches the PetAppearanceForOutfitPreview GQL fragment!
appearance: {
bodyId: String(altStyleData.body_id),
pose: "UNKNOWN",
isGlitched: false,
species: { id: String(altStyleData.species_id) },
color: { id: String(altStyleData.species_id) },
layers: altStyleData.swf_assets.map(normalizeSwfAssetToLayer),
restrictedZones: [],
},
};
}
function normalizeSwfAssetToLayer(swfAssetData) {
return {
id: String(swfAssetData.id),
zone: {
id: String(swfAssetData.zone.id),
depth: swfAssetData.zone.depth,
label: swfAssetData.zone.label,
},
bodyId: swfAssetData.body_id,
knownGlitches: [], // TODO
// HACK: We're just simplifying this adapter, but it would be better to
// actually check what file formats the manifest says!
// TODO: For example, these do generally have SVGs, we could use them!
svgUrl: null,
canvasMovieLibraryUrl: null,
imageUrl: swfAssetData.image_url,
swfUrl: swfAssetData.url,
}; };
} }

View file

@ -46,7 +46,7 @@ class SwfAsset < ApplicationRecord
size_key = size.join('x') size_key = size.join('x')
image_dir = "#{self['type']}/#{partition_path}#{self.remote_id}" image_dir = "#{self['type']}/#{partition_path}#{self.remote_id}"
"//#{host}/#{image_dir}/#{size_key}.png?#{image_version}" "https://#{host}/#{image_dir}/#{size_key}.png?#{image_version}"
end end
def images def images