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",
|
||||
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!
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,15 +62,21 @@ 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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue