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",
null
);
const [preferredColorId, setPreferredColorId] = useLocalStorage(
"DTIItemPreviewPreferredColorId",
null
);
const setPetStateFromUserAction = (petState) => {
setPetState(petState);
const setPetStateFromUserAction = (newPetState) => {
setPetState(newPetState);
// When the user _intentionally_ chooses a species, save it in local
// storage for next time. (This won't update when e.g. their preferred
// species isn't available for this item, so we update to the canonical
// species automatically.)
if (petState.speciesId) {
// I have no reason to expect null to come in here, but, since this is
// touching client-persisted data, I want it to be even more guaranteed
// reliable than usual!
setPreferredSpeciesId(petState.speciesId);
// When the user _intentionally_ chooses a species or color, save it in
// local storage for next time. (This won't update when e.g. their
// preferred species or color isn't available for this item, so we update
// to the canonical species or color automatically.)
//
// Re the "ifs", I have no reason to expect null to come in here, but,
// since this is touching client-persisted data, I want it to be even more
// reliable than usual!
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.)
const { loading: loadingGQL, error: errorGQL, data } = useQuery(
gql`
query ItemPageOutfitPreview($itemId: ID!, $preferredSpeciesId: ID) {
query ItemPageOutfitPreview(
$itemId: ID!
$preferredSpeciesId: ID
$preferredColorId: ID
) {
item(id: $itemId) {
id
name
@ -552,12 +572,15 @@ function ItemPageOutfitPreview({ itemId }) {
id
representsAllBodies
}
canonicalAppearance(preferredSpeciesId: $preferredSpeciesId) {
canonicalAppearance(
preferredSpeciesId: $preferredSpeciesId
preferredColorId: $preferredColorId
) {
id
...ItemAppearanceForOutfitPreview
body {
id
canonicalAppearance {
canonicalAppearance(preferredColorId: $preferredColorId) {
id
species {
id
@ -579,7 +602,7 @@ function ItemPageOutfitPreview({ itemId }) {
${petAppearanceFragment}
`,
{
variables: { itemId, preferredSpeciesId },
variables: { itemId, preferredSpeciesId, preferredColorId },
onCompleted: (data) => {
const canonicalBody = data?.item?.canonicalAppearance?.body;
const canonicalPetAppearance = canonicalBody?.canonicalAppearance;
@ -834,9 +857,7 @@ function SpeciesFacesPicker({
// 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 semantics of returning our colorful random set could be weird…
const selectedColorIsBasic = ["8", "34", "61", "84"].includes(
selectedColorId
);
const selectedColorIsBasic = colorIsBasic(selectedColorId);
const { loading: loadingGQL, error, data } = useQuery(
gql`
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
// 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!

View file

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

View file

@ -1,5 +1,11 @@
import { gql } from "apollo-server";
import { getRestrictedZoneIds, oneWeek, oneDay, oneHour } from "../util";
import {
getRestrictedZoneIds,
normalizeRow,
oneWeek,
oneDay,
oneHour,
} from "../util";
const typeDefs = gql`
type Item @cacheControl(maxAge: ${oneDay}, staleWhileRevalidate: ${oneWeek}) {
@ -56,16 +62,22 @@ const typeDefs = gql`
speciesThatNeedModels(colorId: ID): [Species!]! @cacheControl(maxAge: 1)
# 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
# on the item page, to initialize the preview section. (You can find out
# which species this is for by going through the body field on
# ItemAppearance!)
# with the smallest ID for which we have item appearance data, and a basic
# color. We use this on the item page, to initialize the preview section.
# (You can find out which species this is for by going through the body
# field on ItemAppearance!)
#
# There's also an optional preferredSpeciesId field, which you can use to
# request a certain species if compatible. If not, we'll fall back to the
# default species, as described above.
canonicalAppearance(preferredSpeciesId: ID): ItemAppearance @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
# There's also optional fields preferredSpeciesId and preferredColorId, to
# request a certain species or color if possible. We'll try to match each,
# with precedence to species first; then fall back to the canonical values.
#
# 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
# a union of zones for all of its appearances! We use this for overview
# info about the item.
@ -317,14 +329,37 @@ const resolvers = {
},
canonicalAppearance: async (
{ id },
{ preferredSpeciesId },
{ itemBodiesWithAppearanceDataLoader }
{ preferredSpeciesId, preferredColorId },
{ db }
) => {
const rows = await itemBodiesWithAppearanceDataLoader.load(id);
const preferredRow = preferredSpeciesId
? rows.find((row) => row.speciesId === preferredSpeciesId)
: null;
const bestRow = preferredRow || rows[0];
const [rows, _] = await db.query(
`
SELECT pet_types.body_id, pet_types.species_id FROM pet_types
INNER JOIN colors ON
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 {
item: { id },
bodyId: bestRow.bodyId,

View file

@ -47,8 +47,9 @@ const typeDefs = gql`
id: ID!
species: Species!
# A PetAppearance that has this body. Prefers Blue and happy poses.
canonicalAppearance: PetAppearance
# A PetAppearance that has this body. Prefers Blue (or the optional
# preferredColorId), and happy poses.
canonicalAppearance(preferredColorId: ID): PetAppearance
# Whether this is the special body type that represents fitting _all_ pets.
representsAllBodies: Boolean!
@ -167,11 +168,15 @@ const resolvers = {
return id == "0";
},
canonicalAppearance: async (
{ id },
_,
{ id, species },
{ preferredColorId },
{ canonicalPetStateForBodyLoader }
) => {
const petState = await canonicalPetStateForBodyLoader.load(id);
const petState = await canonicalPetStateForBodyLoader.load({
bodyId: id,
preferredColorId,
fallbackColorId: FALLBACK_COLOR_IDS[species.id] || "8",
});
if (!petState) {
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 };