PosePicker uses real appearance data, ish
This commit is contained in:
parent
7dc01a6feb
commit
da82dba294
5 changed files with 116 additions and 19 deletions
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue