Show selected color faces for SpeciesFacesPicker

This commit is contained in:
Emi Matchu 2021-02-02 23:29:06 -08:00
parent 19482be2b8
commit a42e696955
3 changed files with 157 additions and 7 deletions

View file

@ -18,6 +18,7 @@ import {
Stack,
Wrap,
WrapItem,
Flex,
} from "@chakra-ui/react";
import {
CheckIcon,
@ -25,6 +26,7 @@ import {
EditIcon,
StarIcon,
WarningIcon,
WarningTwoIcon,
} from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";
import gql from "graphql-tag";
@ -717,6 +719,7 @@ function ItemPageOutfitPreview({ itemId }) {
<Box maxWidth="400px">
<SpeciesFacesPicker
selectedSpeciesId={petState.speciesId}
selectedColorId={petState.colorId}
compatibleBodies={compatibleBodies}
couldProbablyModelMoreData={couldProbablyModelMoreData}
onChange={({ speciesId, colorId }) =>
@ -785,22 +788,73 @@ function PlayPauseButton({ isPaused, onClick }) {
function SpeciesFacesPicker({
selectedSpeciesId,
selectedColorId,
compatibleBodies,
couldProbablyModelMoreData,
onChange,
isLoading,
}) {
// For basic colors (Blue, Green, Red, Yellow), we just use the hardcoded
// data, which is part of the bundle and loads super-fast. For other colors,
// we load in all the faces of that color, falling back to basic colors when
// absent!
//
// 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 { loading: loadingGQL, error, data } = useQuery(
gql`
query SpeciesFacesPicker($selectedColorId: ID!) {
color(id: $selectedColorId) {
id
appliedToAllCompatibleSpecies {
id
neopetsImageHash
species {
id
}
body {
id
}
}
}
}
`,
{
variables: { selectedColorId },
skip: selectedColorId == null || selectedColorIsBasic,
onError: (e) => console.error(e),
}
);
const allBodiesAreCompatible = compatibleBodies.some(
(body) => body.representsAllBodies
);
const compatibleBodyIds = compatibleBodies.map((body) => body.id);
const allSpeciesFaces = speciesFaces.sort((a, b) =>
a.speciesName.localeCompare(b.speciesName)
);
const speciesFacesFromData = data?.color?.appliedToAllCompatibleSpecies || [];
const allSpeciesFaces = DEFAULT_SPECIES_FACES.map((defaultSpeciesFace) => {
const providedSpeciesFace = speciesFacesFromData.find(
(f) => f.species.id === defaultSpeciesFace.speciesId
);
if (providedSpeciesFace && providedSpeciesFace.neopetsImageHash) {
return {
...defaultSpeciesFace,
colorId: selectedColorId,
bodyId: providedSpeciesFace.body.id,
neopetsImageHash: providedSpeciesFace.neopetsImageHash,
};
} else {
return defaultSpeciesFace;
}
});
return (
<>
<Box>
<Wrap
spacing="0"
justify="center"
@ -824,11 +878,29 @@ function SpeciesFacesPicker({
isSelected={speciesFace.speciesId === selectedSpeciesId}
couldProbablyModelMoreData={couldProbablyModelMoreData}
onChange={onChange}
isLoading={isLoading}
isLoading={isLoading || loadingGQL}
/>
</WrapItem>
))}
</Wrap>
{error && (
<Flex
color="yellow.500"
fontSize="xs"
marginTop="1"
textAlign="center"
width="100%"
align="flex-start"
justify="center"
>
<WarningTwoIcon marginTop="0.4em" marginRight="1" />
<Box>
Error loading this color's thumbnail images.
<br />
Check your connection and try again.
</Box>
</Flex>
)}
<Global
// Workaround for https://github.com/chakra-ui/chakra-ui/issues/3257,
// which causes tooltip hover flicker.
@ -838,7 +910,7 @@ function SpeciesFacesPicker({
}
`}
/>
</>
</Box>
);
}
@ -1062,7 +1134,7 @@ function DeferredTooltip({ children, isOpen, ...props }) {
// And it's not so bad if this gets out of sync with the database,
// because the SpeciesColorPicker will still be usable!
const colors = { BLUE: "8", RED: "61", GREEN: "34", YELLOW: "84" };
const speciesFaces = [
const DEFAULT_SPECIES_FACES = [
{
speciesName: "Acara",
speciesId: "1",

View file

@ -742,6 +742,29 @@ const buildPetTypeBySpeciesAndColorLoader = (db, loaders) =>
{ cacheKeyFn: ({ speciesId, colorId }) => `${speciesId},${colorId}` }
);
const buildPetTypesForColorLoader = (db, loaders) =>
new DataLoader(async (colorIds) => {
const qs = colorIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM pet_types WHERE color_id IN (${qs})`,
colorIds
);
const entities = rows.map(normalizeRow);
for (const petType of entities) {
loaders.petTypeLoader.prime(petType.id, petType);
loaders.petTypeBySpeciesAndColorLoader.prime(
{ speciesId: petType.speciesId, colorId: petType.colorId },
petType
);
}
return colorIds.map((colorId) =>
entities.filter((e) => e.colorId === colorId)
);
});
const buildSwfAssetLoader = (db) =>
new DataLoader(async (swfAssetIds) => {
const qs = swfAssetIds.map((_) => "?").join(",");
@ -1271,6 +1294,7 @@ function buildLoaders(db) {
db,
loaders
);
loaders.petTypesForColorLoader = buildPetTypesForColorLoader(db, loaders);
loaders.swfAssetLoader = buildSwfAssetLoader(db);
loaders.swfAssetCountLoader = buildSwfAssetCountLoader(db);
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);

View file

@ -14,6 +14,9 @@ const typeDefs = gql`
id: ID!
name: String!
isStandard: Boolean!
# All SpeciesColorPairs of this color.
appliedToAllCompatibleSpecies: [SpeciesColorPair!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneDay})
}
type Species @cacheControl(maxAge: ${oneWeek}) {
@ -68,6 +71,22 @@ const typeDefs = gql`
isGlitched: Boolean!
}
# Like a PetAppearance, but with no pose specified. Species and color are
# enough info to specify a body; and the neopetsImageHash values we save
# don't have gender presentation specified, anyway, so they're available
# here.
type SpeciesColorPair {
id: ID!
species: Species!
color: Color!
body: Body!
# A hash to use in a pets.neopets.com image URL. Might be null if we don't
# have one for this pair, which is uncommon - but it's _somewhat_ common
# for them to have clothes, if we've never seen a plain version modeled.
neopetsImageHash: String
}
extend type Query {
color(id: ID!): Color
allColors: [Color!]! @cacheControl(maxAge: ${oneHour}, staleWhileRevalidate: ${oneWeek})
@ -98,6 +117,15 @@ const resolvers = {
const color = await colorLoader.load(id);
return color.standard ? true : false;
},
appliedToAllCompatibleSpecies: async (
{ id },
_,
{ petTypesForColorLoader }
) => {
const petTypes = await petTypesForColorLoader.load(id);
const speciesColorPairs = petTypes.map((petType) => ({ id: petType.id }));
return speciesColorPairs;
},
},
Species: {
@ -193,6 +221,32 @@ const resolvers = {
},
},
SpeciesColorPair: {
species: async ({ id }, _, { petTypeLoader }) => {
const petType = await petTypeLoader.load(id);
return { id: petType.speciesId };
},
color: async ({ id }, _, { petTypeLoader }) => {
const petType = await petTypeLoader.load(id);
return { id: petType.colorId };
},
body: async ({ id }, _, { petTypeLoader }) => {
const petType = await petTypeLoader.load(id);
return { id: petType.bodyId };
},
neopetsImageHash: async ({ id }, _, { petTypeLoader }) => {
const petType = await petTypeLoader.load(id);
// `basicImageHash` is guaranteed to be a plain no-clothes image, whereas
// `imageHash` prefers to be if possible, but might not be. (I forget the
// details on how this was implemented in Classic, so I'm not _sure_ on
// `imageHash` preferences during modeling… but I'm confident that
// `basicImageHash` is always better, and `imageHash` is better than
// nothing!)
return petType.basicImageHash || petType.imageHash;
},
},
Query: {
color: async (_, { id }, { colorLoader }) => {
const color = await colorLoader.load(id);