Save user's preferred color for item previews

This commit is contained in:
Emi Matchu 2021-02-03 15:24:12 -08:00
parent 787dc7da87
commit be151ab400
4 changed files with 201 additions and 69 deletions

View file

@ -515,19 +515,35 @@ function ItemPageOutfitPreview({ itemId }) {
"DTIItemPreviewPreferredSpeciesId", "DTIItemPreviewPreferredSpeciesId",
null null
); );
const [preferredColorId, setPreferredColorId] = useLocalStorage(
"DTIItemPreviewPreferredColorId",
null
);
const setPetStateFromUserAction = (petState) => { const setPetStateFromUserAction = (newPetState) => {
setPetState(petState); setPetState(newPetState);
// When the user _intentionally_ chooses a species, save it in local // When the user _intentionally_ chooses a species or color, save it in
// storage for next time. (This won't update when e.g. their preferred // local storage for next time. (This won't update when e.g. their
// species isn't available for this item, so we update to the canonical // preferred species or color isn't available for this item, so we update
// species automatically.) // to the canonical species or color automatically.)
if (petState.speciesId) { //
// I have no reason to expect null to come in here, but, since this is // Re the "ifs", I have no reason to expect null to come in here, but,
// touching client-persisted data, I want it to be even more guaranteed // since this is touching client-persisted data, I want it to be even more
// reliable than usual! // reliable than usual!
setPreferredSpeciesId(petState.speciesId); if (newPetState.speciesId && newPetState.speciesId !== petState.speciesId) {
setPreferredSpeciesId(newPetState.speciesId);
}
if (newPetState.colorId && newPetState.colorId !== petState.colorId) {
if (colorIsBasic(newPetState.colorId)) {
// When the user chooses a basic color, don't index on it specifically,
// and instead reset to use default colors.
console.log("set to null");
setPreferredColorId(null);
} else {
console.log("set to color", newPetState.colorId);
setPreferredColorId(newPetState.colorId);
}
} }
}; };
@ -544,7 +560,11 @@ function ItemPageOutfitPreview({ itemId }) {
// cover standard-color switches works for this preloading too.) // cover standard-color switches works for this preloading too.)
const { loading: loadingGQL, error: errorGQL, data } = useQuery( const { loading: loadingGQL, error: errorGQL, data } = useQuery(
gql` gql`
query ItemPageOutfitPreview($itemId: ID!, $preferredSpeciesId: ID) { query ItemPageOutfitPreview(
$itemId: ID!
$preferredSpeciesId: ID
$preferredColorId: ID
) {
item(id: $itemId) { item(id: $itemId) {
id id
name name
@ -552,12 +572,15 @@ function ItemPageOutfitPreview({ itemId }) {
id id
representsAllBodies representsAllBodies
} }
canonicalAppearance(preferredSpeciesId: $preferredSpeciesId) { canonicalAppearance(
preferredSpeciesId: $preferredSpeciesId
preferredColorId: $preferredColorId
) {
id id
...ItemAppearanceForOutfitPreview ...ItemAppearanceForOutfitPreview
body { body {
id id
canonicalAppearance { canonicalAppearance(preferredColorId: $preferredColorId) {
id id
species { species {
id id
@ -579,7 +602,7 @@ function ItemPageOutfitPreview({ itemId }) {
${petAppearanceFragment} ${petAppearanceFragment}
`, `,
{ {
variables: { itemId, preferredSpeciesId }, variables: { itemId, preferredSpeciesId, preferredColorId },
onCompleted: (data) => { onCompleted: (data) => {
const canonicalBody = data?.item?.canonicalAppearance?.body; const canonicalBody = data?.item?.canonicalAppearance?.body;
const canonicalPetAppearance = canonicalBody?.canonicalAppearance; const canonicalPetAppearance = canonicalBody?.canonicalAppearance;
@ -834,9 +857,7 @@ function SpeciesFacesPicker({
// TODO: Could we move this into our `build-cached-data` script, and just do // TODO: Could we move this into our `build-cached-data` script, and just do
// the query all the time, and have Apollo happen to satisfy it fast? // the query all the time, and have Apollo happen to satisfy it fast?
// The semantics of returning our colorful random set could be weird… // The semantics of returning our colorful random set could be weird…
const selectedColorIsBasic = ["8", "34", "61", "84"].includes( const selectedColorIsBasic = colorIsBasic(selectedColorId);
selectedColorId
);
const { loading: loadingGQL, error, data } = useQuery( const { loading: loadingGQL, error, data } = useQuery(
gql` gql`
query SpeciesFacesPicker($selectedColorId: ID!) { query SpeciesFacesPicker($selectedColorId: ID!) {
@ -1321,6 +1342,10 @@ function DeferredTooltip({ children, isOpen, ...props }) {
); );
} }
function colorIsBasic(colorId) {
return ["8", "34", "61", "84"].includes(colorId);
}
// HACK: I'm just hardcoding all this, rather than connecting up to the // HACK: I'm just hardcoding all this, rather than connecting up to the
// database and adding a loading state. Tbh I'm not sure it's a good idea // database and adding a loading state. Tbh I'm not sure it's a good idea
// to load this dynamically until we have SSR to make it come in fast! // to load this dynamically until we have SSR to make it come in fast!

View file

@ -977,47 +977,54 @@ const buildPetStatesForPetTypeLoader = (db, loaders) =>
/** Given a bodyId, loads the canonical PetState to show as an example. */ /** Given a bodyId, loads the canonical PetState to show as an example. */
const buildCanonicalPetStateForBodyLoader = (db, loaders) => const buildCanonicalPetStateForBodyLoader = (db, loaders) =>
new DataLoader(async (bodyIds) => { new DataLoader(
// I don't know how to do this query in bulk, so we'll just do it in async (requests) => {
// parallel! // I don't know how to do this query in bulk, so we'll just do it in
return await Promise.all( // parallel!
bodyIds.map(async (bodyId) => { return await Promise.all(
// Randomly-ish choose which gender presentation to prefer, based on requests.map(async ({ bodyId, preferredColorId, fallbackColorId }) => {
// body ID. This makes the outcome stable, which is nice for caching // Randomly-ish choose which gender presentation to prefer, based on
// and testing and just generally not being surprised, but sitll // body ID. This makes the outcome stable, which is nice for caching
// creates an even distribution. // and testing and just generally not being surprised, but sitll
const gender = bodyId % 2 === 0 ? "masc" : "fem"; // creates an even distribution.
const gender = bodyId % 2 === 0 ? "masc" : "fem";
const [rows, _] = await db.execute( const [rows, _] = await db.execute(
{ {
sql: ` sql: `
SELECT pet_states.*, pet_types.* FROM pet_states SELECT pet_states.*, pet_types.* FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE pet_types.body_id = ? WHERE pet_types.body_id = ?
ORDER BY ORDER BY
pet_types.color_id = 8 DESC, -- Prefer Blue pet_types.color_id = ? DESC, -- Prefer preferredColorId
pet_types.color_id = ? DESC, -- Prefer fallbackColorId
pet_states.mood_id = 1 DESC, -- Prefer Happy pet_states.mood_id = 1 DESC, -- Prefer Happy
pet_states.female = ? DESC, -- Prefer given gender pet_states.female = ? DESC, -- Prefer given gender
pet_states.id DESC, -- Prefer recent models (like in the app) pet_states.id DESC, -- Prefer recent models (like in the app)
pet_states.glitched ASC -- Prefer not glitched (like in the app) pet_states.glitched ASC -- Prefer not glitched (like in the app)
LIMIT 1`, LIMIT 1`,
nestTables: true, nestTables: true,
}, },
[bodyId, gender === "fem"] [bodyId, preferredColorId, fallbackColorId, gender === "fem"]
); );
const petState = normalizeRow(rows[0].pet_states); const petState = normalizeRow(rows[0].pet_states);
const petType = normalizeRow(rows[0].pet_types); const petType = normalizeRow(rows[0].pet_types);
if (!petState || !petType) { if (!petState || !petType) {
return null; return null;
} }
loaders.petStateLoader.prime(petState.id, petState); loaders.petStateLoader.prime(petState.id, petState);
loaders.petTypeLoader.prime(petType.id, petType); loaders.petTypeLoader.prime(petType.id, petType);
return petState; return petState;
}) })
); );
}); },
{
cacheKeyFn: ({ bodyId, preferredColorId, fallbackColorId }) =>
`${bodyId}-${preferredColorId}-${fallbackColorId}`,
}
);
const buildPetStateByPetTypeAndAssetsLoader = (db, loaders) => const buildPetStateByPetTypeAndAssetsLoader = (db, loaders) =>
new DataLoader( new DataLoader(

View file

@ -1,5 +1,11 @@
import { gql } from "apollo-server"; import { gql } from "apollo-server";
import { getRestrictedZoneIds, oneWeek, oneDay, oneHour } from "../util"; import {
getRestrictedZoneIds,
normalizeRow,
oneWeek,
oneDay,
oneHour,
} from "../util";
const typeDefs = gql` const typeDefs = gql`
type Item @cacheControl(maxAge: ${oneDay}, staleWhileRevalidate: ${oneWeek}) { type Item @cacheControl(maxAge: ${oneDay}, staleWhileRevalidate: ${oneWeek}) {
@ -56,16 +62,22 @@ const typeDefs = gql`
speciesThatNeedModels(colorId: ID): [Species!]! @cacheControl(maxAge: 1) speciesThatNeedModels(colorId: ID): [Species!]! @cacheControl(maxAge: 1)
# Return a single ItemAppearance for this item. It'll be for the species # Return a single ItemAppearance for this item. It'll be for the species
# with the smallest ID for which we have item appearance data. We use this # with the smallest ID for which we have item appearance data, and a basic
# on the item page, to initialize the preview section. (You can find out # color. We use this on the item page, to initialize the preview section.
# which species this is for by going through the body field on # (You can find out which species this is for by going through the body
# ItemAppearance!) # field on ItemAppearance!)
# #
# There's also an optional preferredSpeciesId field, which you can use to # There's also optional fields preferredSpeciesId and preferredColorId, to
# request a certain species if compatible. If not, we'll fall back to the # request a certain species or color if possible. We'll try to match each,
# default species, as described above. # with precedence to species first; then fall back to the canonical values.
canonicalAppearance(preferredSpeciesId: ID): ItemAppearance @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek}) #
# Note that the exact choice of color doesn't usually affect this field,
# because ItemAppearance is per-body rather than per-color. It's most
# relevant for special colors like Baby or Mutant. But the
# canonicalAppearance on the Body type _does_ use the preferred color more
# precisely!
canonicalAppearance(preferredSpeciesId: ID, preferredColorId: ID): ItemAppearance @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
# All zones that this item occupies, for at least one body. That is, it's # All zones that this item occupies, for at least one body. That is, it's
# a union of zones for all of its appearances! We use this for overview # a union of zones for all of its appearances! We use this for overview
# info about the item. # info about the item.
@ -317,14 +329,37 @@ const resolvers = {
}, },
canonicalAppearance: async ( canonicalAppearance: async (
{ id }, { id },
{ preferredSpeciesId }, { preferredSpeciesId, preferredColorId },
{ itemBodiesWithAppearanceDataLoader } { db }
) => { ) => {
const rows = await itemBodiesWithAppearanceDataLoader.load(id); const [rows, _] = await db.query(
const preferredRow = preferredSpeciesId `
? rows.find((row) => row.speciesId === preferredSpeciesId) SELECT pet_types.body_id, pet_types.species_id FROM pet_types
: null; INNER JOIN colors ON
const bestRow = preferredRow || rows[0]; pet_types.color_id = colors.id
INNER JOIN swf_assets ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
INNER JOIN parents_swf_assets ON
parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN items ON
items.id = parents_swf_assets.parent_id AND
parents_swf_assets.parent_type = "Item"
WHERE items.id = ?
ORDER BY
pet_types.species_id = ? DESC,
pet_types.color_id = ? DESC,
pet_types.species_id ASC,
colors.standard DESC
LIMIT 1
`,
[id, preferredSpeciesId || "<ignore>", preferredColorId || "<ignore>"]
);
if (rows.length === 0) {
return null;
}
const bestRow = normalizeRow(rows[0]);
return { return {
item: { id }, item: { id },
bodyId: bestRow.bodyId, bodyId: bestRow.bodyId,

View file

@ -47,8 +47,9 @@ const typeDefs = gql`
id: ID! id: ID!
species: Species! species: Species!
# A PetAppearance that has this body. Prefers Blue and happy poses. # A PetAppearance that has this body. Prefers Blue (or the optional
canonicalAppearance: PetAppearance # preferredColorId), and happy poses.
canonicalAppearance(preferredColorId: ID): PetAppearance
# Whether this is the special body type that represents fitting _all_ pets. # Whether this is the special body type that represents fitting _all_ pets.
representsAllBodies: Boolean! representsAllBodies: Boolean!
@ -167,11 +168,15 @@ const resolvers = {
return id == "0"; return id == "0";
}, },
canonicalAppearance: async ( canonicalAppearance: async (
{ id }, { id, species },
_, { preferredColorId },
{ canonicalPetStateForBodyLoader } { canonicalPetStateForBodyLoader }
) => { ) => {
const petState = await canonicalPetStateForBodyLoader.load(id); const petState = await canonicalPetStateForBodyLoader.load({
bodyId: id,
preferredColorId,
fallbackColorId: FALLBACK_COLOR_IDS[species.id] || "8",
});
if (!petState) { if (!petState) {
return null; return null;
} }
@ -309,4 +314,64 @@ const resolvers = {
}, },
}; };
// NOTE: This matches the colors on ItemPage, so that they always match!
const colors = { BLUE: "8", RED: "61", GREEN: "34", YELLOW: "84" };
const FALLBACK_COLOR_IDS = {
"1": colors.GREEN, // Acara
"2": colors.BLUE, // Aisha
"3": colors.YELLOW, // Blumaroo
"4": colors.YELLOW, // Bori
"5": colors.YELLOW, // Bruce
"6": colors.YELLOW, // Buzz
"7": colors.RED, // Chia
"8": colors.YELLOW, // Chomby
"9": colors.GREEN, // Cybunny
"10": colors.YELLOW, // Draik
"11": colors.RED, // Elephante
"12": colors.RED, // Eyrie
"13": colors.GREEN, // Flotsam
"14": colors.YELLOW, // Gelert
"15": colors.BLUE, // Gnorbu
"16": colors.BLUE, // Grarrl
"17": colors.GREEN, // Grundo
"18": colors.RED, // Hissi
"19": colors.GREEN, // Ixi
"20": colors.YELLOW, // Jetsam
"21": colors.GREEN, // Jubjub
"22": colors.YELLOW, // Kacheek
"23": colors.BLUE, // Kau
"24": colors.GREEN, // Kiko
"25": colors.GREEN, // Koi
"26": colors.RED, // Korbat
"27": colors.BLUE, // Kougra
"28": colors.BLUE, // Krawk
"29": colors.YELLOW, // Kyrii
"30": colors.YELLOW, // Lenny
"31": colors.YELLOW, // Lupe
"32": colors.BLUE, // Lutari
"33": colors.YELLOW, // Meerca
"34": colors.GREEN, // Moehog
"35": colors.BLUE, // Mynci
"36": colors.BLUE, // Nimmo
"37": colors.YELLOW, // Ogrin
"38": colors.RED, // Peophin
"39": colors.GREEN, // Poogle
"40": colors.RED, // Pteri
"41": colors.YELLOW, // Quiggle
"42": colors.BLUE, // Ruki
"43": colors.RED, // Scorchio
"44": colors.YELLOW, // Shoyru
"45": colors.RED, // Skeith
"46": colors.YELLOW, // Techo
"47": colors.BLUE, // Tonu
"48": colors.YELLOW, // Tuskaninny
"49": colors.GREEN, // Uni
"50": colors.RED, // Usul
"55": colors.YELLOW, // Vandagyre
"51": colors.YELLOW, // Wocky
"52": colors.RED, // Xweetok
"53": colors.RED, // Yurble
"54": colors.BLUE, // Zafara
};
module.exports = { typeDefs, resolvers }; module.exports = { typeDefs, resolvers };