Save user's preferred color for item previews
This commit is contained in:
parent
787dc7da87
commit
be151ab400
4 changed files with 201 additions and 69 deletions
|
@ -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!
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
Loading…
Reference in a new issue