Show selected color faces for SpeciesFacesPicker
This commit is contained in:
parent
19482be2b8
commit
a42e696955
3 changed files with 157 additions and 7 deletions
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue