PosePicker uses real appearance data, ish

This commit is contained in:
Matt Dunn-Rankin 2020-05-02 17:22:46 -07:00
parent 7dc01a6feb
commit da82dba294
5 changed files with 116 additions and 19 deletions

View file

@ -82,6 +82,7 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
</Box> </Box>
<Flex flex="1 1 0" align="center" pl="4"> <Flex flex="1 1 0" align="center" pl="4">
<PosePicker <PosePicker
outfitState={outfitState}
onLockFocus={() => setFocusIsLocked(true)} onLockFocus={() => setFocusIsLocked(true)}
onUnlockFocus={() => setFocusIsLocked(false)} onUnlockFocus={() => setFocusIsLocked(false)}
/> />

View file

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/react-hooks";
import { css, cx } from "emotion"; import { css, cx } from "emotion";
import { import {
Box, Box,
@ -12,6 +14,7 @@ import {
useTheme, useTheme,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { petAppearanceFragment } from "./useOutfitAppearance";
import { safeImageUrl } from "./util"; import { safeImageUrl } from "./util";
// From https://twemoji.twitter.com/, thank you! // From https://twemoji.twitter.com/, thank you!
@ -21,9 +24,32 @@ import twemojiSick from "../images/twemoji/sick.svg";
import twemojiMasc from "../images/twemoji/masc.svg"; import twemojiMasc from "../images/twemoji/masc.svg";
import twemojiFem from "../images/twemoji/fem.svg"; import twemojiFem from "../images/twemoji/fem.svg";
function PosePicker({ onLockFocus, onUnlockFocus }) { function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) {
const theme = useTheme(); const theme = useTheme();
const { speciesId, colorId } = outfitState;
const { loading, error, poses } = useAvailablePoses({
speciesId,
colorId,
});
if (loading) {
return null;
}
// This is a low-stakes enough control, where enough pairs don't have data
// anyway, that I think I want to just not draw attention to failures.
if (error) {
return null;
}
// If there's only one pose anyway, don't bother showing a picker!
const numAvailablePoses = Object.values(poses).filter((p) => p.isAvailable)
.length;
if (numAvailablePoses <= 1) {
return null;
}
return ( return (
<Popover <Popover
placement="top-end" placement="top-end"
@ -87,13 +113,13 @@ function PosePicker({ onLockFocus, onUnlockFocus }) {
<EmojiImage src={twemojiMasc} aria-label="Masculine" /> <EmojiImage src={twemojiMasc} aria-label="Masculine" />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/1/1.png" /> <PoseButton pose={poses.happyMasc} />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/2/1.png" /> <PoseButton pose={poses.sadMasc} />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/4/1.png" /> <PoseButton pose={poses.sickMasc} />
</Cell> </Cell>
</tr> </tr>
<tr> <tr>
@ -101,13 +127,13 @@ function PosePicker({ onLockFocus, onUnlockFocus }) {
<EmojiImage src={twemojiFem} aria-label="Feminine" /> <EmojiImage src={twemojiFem} aria-label="Feminine" />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/1/1.png" /> <PoseButton pose={poses.happyFem} />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/2/1.png" /> <PoseButton pose={poses.sadFem} />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/4/1.png" /> <PoseButton pose={poses.sickFem} />
</Cell> </Cell>
</tr> </tr>
</tbody> </tbody>
@ -132,12 +158,16 @@ function Cell({ children, as }) {
); );
} }
function PoseButton({ src }) { function PoseButton({ pose }) {
if (!pose.isAvailable) {
return null;
}
return ( return (
<Box rounded="full" boxShadow="md" overflow="hidden"> <Box rounded="full" boxShadow="md" overflow="hidden">
<Button variant="unstyled" width="auto" height="auto"> <Button variant="unstyled" width="auto" height="auto">
<Image <Image
src={safeImageUrl(src)} src={safeImageUrl(pose.thumbnailUrl)}
width="50px" width="50px"
height="50px" height="50px"
className={css` className={css`
@ -158,4 +188,55 @@ function EmojiImage({ src, "aria-label": ariaLabel }) {
return <Image src={src} aria-label={ariaLabel} width="16px" height="16px" />; return <Image src={src} aria-label={ariaLabel} width="16px" height="16px" />;
} }
function useAvailablePoses({ speciesId, colorId }) {
const { loading, error, data } = useQuery(
gql`
query PosePicker($speciesId: ID!, $colorId: ID!) {
petAppearances(speciesId: $speciesId, colorId: $colorId) {
genderPresentation
emotion
...PetAppearanceForOutfitPreview
}
}
${petAppearanceFragment}
`,
{ variables: { speciesId, colorId } }
);
const petAppearances = data?.petAppearances || [];
const hasAppearanceFor = (e, gp) =>
petAppearances.some(
(pa) => pa.emotion === e && pa.genderPresentation === gp
);
const poses = {
happyMasc: {
isAvailable: hasAppearanceFor("HAPPY", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/1/1.png",
},
sadMasc: {
isAvailable: hasAppearanceFor("SAD", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/2/1.png",
},
sickMasc: {
isAvailable: hasAppearanceFor("SICK", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/4/1.png",
},
happyFem: {
isAvailable: hasAppearanceFor("HAPPY", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/1/1.png",
},
sadFem: {
isAvailable: hasAppearanceFor("SAD", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/2/1.png",
},
sickFem: {
isAvailable: hasAppearanceFor("SICK", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/4/1.png",
},
};
return { loading, error, poses };
}
export default PosePicker; export default PosePicker;

View file

@ -227,7 +227,7 @@ function useSearchResults(query, outfitState) {
appearanceOn(speciesId: $speciesId, colorId: $colorId) { appearanceOn(speciesId: $speciesId, colorId: $colorId) {
# This enables us to quickly show the item when the user clicks it! # This enables us to quickly show the item when the user clicks it!
...AppearanceForOutfitPreview ...ItemAppearanceForOutfitPreview
# This is used to group items by zone, and to detect conflicts when # This is used to group items by zone, and to detect conflicts when
# wearing a new item. # wearing a new item.

View file

@ -12,17 +12,18 @@ export default function useOutfitAppearance(outfitState) {
gql` gql`
query($wornItemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) { query($wornItemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) {
petAppearance(speciesId: $speciesId, colorId: $colorId) { petAppearance(speciesId: $speciesId, colorId: $colorId) {
...AppearanceForOutfitPreview ...PetAppearanceForOutfitPreview
} }
items(ids: $wornItemIds) { items(ids: $wornItemIds) {
id id
appearanceOn(speciesId: $speciesId, colorId: $colorId) { appearanceOn(speciesId: $speciesId, colorId: $colorId) {
...AppearanceForOutfitPreview ...ItemAppearanceForOutfitPreview
} }
} }
} }
${itemAppearanceFragment} ${itemAppearanceFragment}
${petAppearanceFragment}
`, `,
{ {
variables: { wornItemIds, speciesId, colorId }, variables: { wornItemIds, speciesId, colorId },
@ -39,10 +40,11 @@ function getVisibleLayers(data) {
return []; return [];
} }
const allAppearances = [ const itemAppearances = (data.items || []).map((i) => i.appearanceOn);
data.petAppearance,
...(data.items || []).map((i) => i.appearanceOn), const allAppearances = [data.petAppearance, ...itemAppearances].filter(
].filter((a) => a); (a) => a
);
let allLayers = allAppearances.map((a) => a.layers).flat(); let allLayers = allAppearances.map((a) => a.layers).flat();
// Clean up our data a bit, by ensuring only one layer per zone. This // Clean up our data a bit, by ensuring only one layer per zone. This
@ -52,7 +54,7 @@ function getVisibleLayers(data) {
return allLayers.findIndex((l2) => l2.zone.id === l.zone.id) === i; return allLayers.findIndex((l2) => l2.zone.id === l.zone.id) === i;
}); });
const allRestrictedZoneIds = allAppearances const allRestrictedZoneIds = itemAppearances
.map((l) => l.restrictedZones) .map((l) => l.restrictedZones)
.flat() .flat()
.map((z) => z.id); .map((z) => z.id);
@ -66,7 +68,7 @@ function getVisibleLayers(data) {
} }
export const itemAppearanceFragment = gql` export const itemAppearanceFragment = gql`
fragment AppearanceForOutfitPreview on Appearance { fragment ItemAppearanceForOutfitPreview on ItemAppearance {
layers { layers {
id id
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
@ -81,3 +83,16 @@ export const itemAppearanceFragment = gql`
} }
} }
`; `;
export const petAppearanceFragment = gql`
fragment PetAppearanceForOutfitPreview on PetAppearance {
layers {
id
imageUrl(size: SIZE_600)
zone {
id
depth
}
}
}
`;

View file

@ -54,7 +54,7 @@ function useOutfitState() {
appearanceOn(speciesId: $speciesId, colorId: $colorId) { appearanceOn(speciesId: $speciesId, colorId: $colorId) {
# This enables us to quickly show the item when the user clicks it! # This enables us to quickly show the item when the user clicks it!
...AppearanceForOutfitPreview ...ItemAppearanceForOutfitPreview
# This is used to group items by zone, and to detect conflicts when # This is used to group items by zone, and to detect conflicts when
# wearing a new item. # wearing a new item.