refactor e/gp pairs to pose enum

This commit is contained in:
Matt Dunn-Rankin 2020-05-23 12:47:06 -07:00
parent 772917fde6
commit 75a0fe2e8c
13 changed files with 210 additions and 182 deletions

View file

@ -128,8 +128,7 @@ function SubmitPetForm() {
name: petName, name: petName,
species: species.id, species: species.id,
color: color.id, color: color.id,
emotion: "HAPPY", // TODO: Ask PetService pose: "HAPPY_FEM", // TODO: Ask PetService
genderPresentation: "FEMININE", // TODO: Ask PetService
}); });
for (const item of items) { for (const item of items) {
params.append("objects[]", item.id); params.append("objects[]", item.id);

View file

@ -32,7 +32,7 @@ function PosePicker({
}) { }) {
const theme = useTheme(); const theme = useTheme();
const checkedInputRef = React.useRef(); const checkedInputRef = React.useRef();
const { loading, error, poses } = usePoses(outfitState); const { loading, error, poseInfos } = usePoses(outfitState);
if (loading) { if (loading) {
return null; return null;
@ -45,19 +45,15 @@ function PosePicker({
} }
// If there's only one pose anyway, don't bother showing a picker! // If there's only one pose anyway, don't bother showing a picker!
const numAvailablePoses = Object.values(poses).filter((p) => p.isAvailable) const numAvailablePoses = Object.values(poseInfos).filter(
.length; (p) => p.isAvailable
).length;
if (numAvailablePoses <= 1) { if (numAvailablePoses <= 1) {
return null; return null;
} }
const onChange = (e) => { const onChange = (e) => {
const [emotion, genderPresentation] = e.target.value.split("-"); dispatchToOutfit({ type: "setPose", pose: e.target.value });
dispatchToOutfit({
type: "setPose",
emotion,
genderPresentation,
});
}; };
return ( return (
@ -99,13 +95,13 @@ function PosePicker({
isOpen && "is-open" isOpen && "is-open"
)} )}
> >
{outfitState.emotion === "HAPPY" && ( {getEmotion(outfitState.pose) === "HAPPY" && (
<EmojiImage src={twemojiSmile} alt="Choose a pose" /> <EmojiImage src={twemojiSmile} alt="Choose a pose" />
)} )}
{outfitState.emotion === "SAD" && ( {getEmotion(outfitState.pose) === "SAD" && (
<EmojiImage src={twemojiCry} alt="Choose a pose" /> <EmojiImage src={twemojiCry} alt="Choose a pose" />
)} )}
{outfitState.emotion === "SICK" && ( {getEmotion(outfitState.pose) === "SICK" && (
<EmojiImage src={twemojiSick} alt="Choose a pose" /> <EmojiImage src={twemojiSick} alt="Choose a pose" />
)} )}
</Button> </Button>
@ -133,24 +129,30 @@ function PosePicker({
<EmojiImage src={twemojiMasc} alt="Masculine" /> <EmojiImage src={twemojiMasc} alt="Masculine" />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.happyMasc} poseInfo={poseInfos.happyMasc}
onChange={onChange} onChange={onChange}
inputRef={poses.happyMasc.isSelected && checkedInputRef} inputRef={
poseInfos.happyMasc.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.sadMasc} poseInfo={poseInfos.sadMasc}
onChange={onChange} onChange={onChange}
inputRef={poses.sadMasc.isSelected && checkedInputRef} inputRef={
poseInfos.sadMasc.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.sickMasc} poseInfo={poseInfos.sickMasc}
onChange={onChange} onChange={onChange}
inputRef={poses.sickMasc.isSelected && checkedInputRef} inputRef={
poseInfos.sickMasc.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
</tr> </tr>
@ -159,24 +161,30 @@ function PosePicker({
<EmojiImage src={twemojiFem} alt="Feminine" /> <EmojiImage src={twemojiFem} alt="Feminine" />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.happyFem} poseInfo={poseInfos.happyFem}
onChange={onChange} onChange={onChange}
inputRef={poses.happyFem.isSelected && checkedInputRef} inputRef={
poseInfos.happyFem.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.sadFem} poseInfo={poseInfos.sadFem}
onChange={onChange} onChange={onChange}
inputRef={poses.sadFem.isSelected && checkedInputRef} inputRef={
poseInfos.sadFem.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
<Cell as="td"> <Cell as="td">
<PoseButton <PoseOption
pose={poses.sickFem} poseInfo={poseInfos.sickFem}
onChange={onChange} onChange={onChange}
inputRef={poses.sickFem.isSelected && checkedInputRef} inputRef={
poseInfos.sickFem.isSelected && checkedInputRef
}
/> />
</Cell> </Cell>
</tr> </tr>
@ -213,14 +221,14 @@ const GENDER_PRESENTATION_STRINGS = {
FEMININE: "Feminine", FEMININE: "Feminine",
}; };
function PoseButton({ pose, onChange, inputRef }) { function PoseOption({ poseInfo, onChange, inputRef }) {
const theme = useTheme(); const theme = useTheme();
const genderPresentationStr = const genderPresentationStr =
GENDER_PRESENTATION_STRINGS[pose.genderPresentation]; GENDER_PRESENTATION_STRINGS[poseInfo.genderPresentation];
const emotionStr = EMOTION_STRINGS[pose.emotion]; const emotionStr = EMOTION_STRINGS[poseInfo.emotion];
let label = `${emotionStr} and ${genderPresentationStr}`; let label = `${emotionStr} and ${genderPresentationStr}`;
if (!pose.isAvailable) { if (!poseInfo.isAvailable) {
label += ` (not modeled yet)`; label += ` (not modeled yet)`;
} }
@ -239,9 +247,9 @@ function PoseButton({ pose, onChange, inputRef }) {
type="radio" type="radio"
aria-label={label} aria-label={label}
name="pose" name="pose"
value={`${pose.emotion}-${pose.genderPresentation}`} value={poseInfo.pose}
checked={pose.isSelected} checked={poseInfo.isSelected}
disabled={!pose.isAvailable} disabled={!poseInfo.isAvailable}
onChange={onChange} onChange={onChange}
ref={inputRef || null} ref={inputRef || null}
/> />
@ -253,11 +261,11 @@ function PoseButton({ pose, onChange, inputRef }) {
width="50px" width="50px"
height="50px" height="50px"
title={ title={
pose.isAvailable poseInfo.isAvailable
? // A lil debug output, so that we can quickly identify glitched ? // A lil debug output, so that we can quickly identify glitched
// PetStates and manually mark them as glitched! // PetStates and manually mark them as glitched!
window.location.hostname.includes("localhost") && window.location.hostname.includes("localhost") &&
`#${pose.petStateId}` `#${poseInfo.petStateId}`
: "Not modeled yet" : "Not modeled yet"
} }
position="relative" position="relative"
@ -298,18 +306,18 @@ function PoseButton({ pose, onChange, inputRef }) {
border-width: 3px; border-width: 3px;
} }
`, `,
!pose.isAvailable && "not-available" !poseInfo.isAvailable && "not-available"
)} )}
/> />
{pose.isAvailable ? ( {poseInfo.isAvailable ? (
<Box <Box
width="50px" width="50px"
height="50px" height="50px"
transform={ transform={
transformsByBodyId[pose.bodyId] || transformsByBodyId.default transformsByBodyId[poseInfo.bodyId] || transformsByBodyId.default
} }
> >
<OutfitLayers visibleLayers={getVisibleLayers(pose, [])} /> <OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} />
</Box> </Box>
) : ( ) : (
<Flex align="center" justify="center"> <Flex align="center" justify="center">
@ -342,8 +350,7 @@ function usePoses(outfitState) {
id id
petStateId petStateId
bodyId bodyId
genderPresentation pose
emotion
approximateThumbnailUrl approximateThumbnailUrl
...PetAppearanceForOutfitPreview ...PetAppearanceForOutfitPreview
} }
@ -354,28 +361,39 @@ function usePoses(outfitState) {
); );
const petAppearances = data?.petAppearances || []; const petAppearances = data?.petAppearances || [];
const buildPose = (e, gp) => { const buildPoseInfo = (pose) => {
const appearance = petAppearances.find( const appearance = petAppearances.find((pa) => pa.pose === pose);
(pa) => pa.emotion === e && pa.genderPresentation === gp
);
return { return {
...appearance, ...appearance,
isAvailable: Boolean(appearance), isAvailable: Boolean(appearance),
isSelected: isSelected: outfitState.pose === pose,
outfitState.emotion === e && outfitState.genderPresentation === gp,
}; };
}; };
const poses = { const poseInfos = {
happyMasc: buildPose("HAPPY", "MASCULINE"), happyMasc: buildPoseInfo("HAPPY_MASC"),
sadMasc: buildPose("SAD", "MASCULINE"), sadMasc: buildPoseInfo("SAD_MASC"),
sickMasc: buildPose("SICK", "MASCULINE"), sickMasc: buildPoseInfo("SICK_MASC"),
happyFem: buildPose("HAPPY", "FEMININE"), happyFem: buildPoseInfo("HAPPY_FEM"),
sadFem: buildPose("SAD", "FEMININE"), sadFem: buildPoseInfo("SAD_FEM"),
sickFem: buildPose("SICK", "FEMININE"), sickFem: buildPoseInfo("SICK_FEM"),
}; };
return { loading, error, poses }; return { loading, error, poseInfos };
}
function getEmotion(pose) {
if (["HAPPY_MASC", "HAPPY_FEM"].includes(pose)) {
return "HAPPY";
} else if (["SAD_MASC", "SAD_FEM"].includes(pose)) {
return "SAD";
} else if (["SICK_MASC", "SICK_FEM"].includes(pose)) {
return "SICK";
} else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
return null;
} else {
throw new Error(`unrecognized pose ${JSON.stringify(pose)}`);
}
} }
const transformsByBodyId = { const transformsByBodyId = {

View file

@ -16,8 +16,8 @@ const cacheRedirects = {
// way, when you switch pet poses, Apollo knows it already has the // way, when you switch pet poses, Apollo knows it already has the
// appearance data and doesn't need to ask the server again! // appearance data and doesn't need to ask the server again!
petAppearance: (_, args, { getCacheKey }) => { petAppearance: (_, args, { getCacheKey }) => {
const { speciesId, colorId, emotion, genderPresentation } = args; const { speciesId, colorId, pose } = args;
const id = `${speciesId}-${colorId}-${emotion}-${genderPresentation}`; const id = `${speciesId}-${colorId}-${pose}`;
return getCacheKey({ __typename: "PetAppearance", id }); return getCacheKey({ __typename: "PetAppearance", id });
}, },
}, },

View file

@ -6,13 +6,7 @@ import { useQuery } from "@apollo/react-hooks";
* visibleLayers for rendering. * visibleLayers for rendering.
*/ */
export default function useOutfitAppearance(outfitState) { export default function useOutfitAppearance(outfitState) {
const { const { wornItemIds, speciesId, colorId, pose } = outfitState;
wornItemIds,
speciesId,
colorId,
emotion,
genderPresentation,
} = outfitState;
const { loading, error, data } = useQuery( const { loading, error, data } = useQuery(
gql` gql`
@ -20,15 +14,9 @@ export default function useOutfitAppearance(outfitState) {
$wornItemIds: [ID!]! $wornItemIds: [ID!]!
$speciesId: ID! $speciesId: ID!
$colorId: ID! $colorId: ID!
$emotion: Emotion! $pose: Pose!
$genderPresentation: GenderPresentation!
) {
petAppearance(
speciesId: $speciesId
colorId: $colorId
emotion: $emotion
genderPresentation: $genderPresentation
) { ) {
petAppearance(speciesId: $speciesId, colorId: $colorId, pose: $pose) {
...PetAppearanceForOutfitPreview ...PetAppearanceForOutfitPreview
} }
@ -47,8 +35,7 @@ export default function useOutfitAppearance(outfitState) {
wornItemIds, wornItemIds,
speciesId, speciesId,
colorId, colorId,
emotion, pose,
genderPresentation,
}, },
} }
); );

View file

@ -15,7 +15,7 @@ function useOutfitState() {
initialState initialState
); );
const { name, speciesId, colorId, emotion, genderPresentation } = state; const { name, speciesId, colorId, pose } = state;
// It's more convenient to manage these as a Set in state, but most callers // It's more convenient to manage these as a Set in state, but most callers
// will find it more convenient to access them as arrays! e.g. for `.map()` // will find it more convenient to access them as arrays! e.g. for `.map()`
@ -84,8 +84,7 @@ function useOutfitState() {
allItemIds, allItemIds,
speciesId, speciesId,
colorId, colorId,
emotion, pose,
genderPresentation,
url, url,
}; };
@ -159,28 +158,21 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
closetedItemIds.delete(itemId); closetedItemIds.delete(itemId);
}); });
case "setPose": case "setPose":
return produce(baseState, (state) => { return { ...baseState, pose: action.pose };
const { emotion, genderPresentation } = action;
state.emotion = emotion;
state.genderPresentation = genderPresentation;
});
case "reset": case "reset":
return produce(baseState, (state) => { return produce(baseState, (state) => {
const { const {
name, name,
speciesId, speciesId,
colorId, colorId,
emotion, pose,
genderPresentation,
wornItemIds, wornItemIds,
closetedItemIds, closetedItemIds,
} = action; } = action;
state.name = name; state.name = name;
state.speciesId = speciesId ? String(speciesId) : baseState.speciesId; state.speciesId = speciesId ? String(speciesId) : baseState.speciesId;
state.colorId = colorId ? String(colorId) : baseState.colorId; state.colorId = colorId ? String(colorId) : baseState.colorId;
state.emotion = emotion || baseState.emotion; state.pose = pose || baseState.pose;
state.genderPresentation =
genderPresentation || baseState.genderPresentation;
state.wornItemIds = wornItemIds state.wornItemIds = wornItemIds
? new Set(wornItemIds.map(String)) ? new Set(wornItemIds.map(String))
: baseState.wornItemIds; : baseState.wornItemIds;
@ -199,8 +191,7 @@ function parseOutfitUrl() {
name: urlParams.get("name"), name: urlParams.get("name"),
speciesId: urlParams.get("species"), speciesId: urlParams.get("species"),
colorId: urlParams.get("color"), colorId: urlParams.get("color"),
emotion: urlParams.get("emotion") || "HAPPY", pose: urlParams.get("pose") || "HAPPY_FEM",
genderPresentation: urlParams.get("genderPresentation") || "FEMININE",
wornItemIds: new Set(urlParams.getAll("objects[]")), wornItemIds: new Set(urlParams.getAll("objects[]")),
closetedItemIds: new Set(urlParams.getAll("closet[]")), closetedItemIds: new Set(urlParams.getAll("closet[]")),
}; };
@ -335,8 +326,7 @@ function buildOutfitUrl(state) {
name, name,
speciesId, speciesId,
colorId, colorId,
emotion, pose,
genderPresentation,
wornItemIds, wornItemIds,
closetedItemIds, closetedItemIds,
} = state; } = state;
@ -345,8 +335,7 @@ function buildOutfitUrl(state) {
name: name || "", name: name || "",
species: speciesId, species: speciesId,
color: colorId, color: colorId,
emotion, pose,
genderPresentation,
}); });
for (const itemId of wornItemIds) { for (const itemId of wornItemIds) {
params.append("objects[]", itemId); params.append("objects[]", itemId);

View file

@ -1,25 +1,25 @@
import connectToDb from "./db"; import connectToDb from "./db";
import { getPose } from "./util"; import { getPoseFromPetState, normalizeRow } from "./util";
export default async function getValidPetPoses() { export default async function getValidPetPoses() {
const db = await connectToDb(); const db = await connectToDb();
const numSpeciesPromise = getNumSpecies(db); const numSpeciesPromise = getNumSpecies(db);
const numColorsPromise = getNumColors(db); const numColorsPromise = getNumColors(db);
const poseTuplesPromise = getPoseTuples(db); const distinctPetStatesPromise = getDistinctPetStates(db);
const [numSpecies, numColors, poseTuples] = await Promise.all([ const [numSpecies, numColors, distinctPetStates] = await Promise.all([
numSpeciesPromise, numSpeciesPromise,
numColorsPromise, numColorsPromise,
poseTuplesPromise, distinctPetStatesPromise,
]); ]);
const poseStrs = new Set(); const poseStrs = new Set();
for (const poseTuple of poseTuples) { for (const petState of distinctPetStates) {
const { species_id, color_id, mood_id, female, unconverted } = poseTuple; const { speciesId, colorId } = petState;
const pose = getPose(mood_id, female, unconverted); const pose = getPoseFromPetState(petState);
const poseStr = `${species_id}-${color_id}-${pose}`; const poseStr = `${speciesId}-${colorId}-${pose}`;
poseStrs.add(poseStr); poseStrs.add(poseStr);
} }
@ -77,11 +77,11 @@ async function getNumColors(db) {
return rows[0]["count(*)"]; return rows[0]["count(*)"];
} }
async function getPoseTuples(db) { async function getDistinctPetStates(db) {
const [rows, _] = await db.query(` const [rows, _] = await db.query(`
SELECT DISTINCT species_id, color_id, mood_id, female, unconverted SELECT DISTINCT species_id, color_id, mood_id, female, unconverted
FROM pet_states FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE glitched IS false AND color_id >= 1`); WHERE glitched IS false AND color_id >= 1`);
return rows; return rows.map(normalizeRow);
} }

View file

@ -3,6 +3,15 @@ import getValidPetPoses from "./getValidPetPoses";
describe("getValidPetPoses", () => { describe("getValidPetPoses", () => {
it("gets them and writes them to a buffer", async () => { it("gets them and writes them to a buffer", async () => {
const buffer = await getValidPetPoses(); const buffer = await getValidPetPoses();
expect(buffer.toString()).toMatchSnapshot(); expect(asBinaryString(buffer)).toMatchSnapshot();
}); });
}); });
function asBinaryString(buffer) {
let str = "";
for (let i = 0; i < buffer.length; i++) {
const byte = buffer.readUInt8(i);
str += byte.toString(2).padStart(8, "0") + "\n";
}
return str;
}

View file

@ -3,7 +3,12 @@ const { gql } = require("apollo-server");
const connectToDb = require("./db"); const connectToDb = require("./db");
const buildLoaders = require("./loaders"); const buildLoaders = require("./loaders");
const neopets = require("./neopets"); const neopets = require("./neopets");
const { capitalize, getEmotion, getGenderPresentation } = require("./util"); const {
capitalize,
getPoseFromPetState,
getEmotion,
getGenderPresentation,
} = require("./util");
const typeDefs = gql` const typeDefs = gql`
enum LayerImageSize { enum LayerImageSize {
@ -12,6 +17,20 @@ const typeDefs = gql`
SIZE_150 SIZE_150
} }
"""
The poses a PetAppearance can take!
"""
enum Pose {
HAPPY_MASC
SAD_MASC
SICK_MASC
HAPPY_FEM
SAD_FEM
SICK_FEM
UNCONVERTED
UNKNOWN # for when we have the data, but we don't know what it is
}
""" """
A pet's gender presentation: masculine or feminine. A pet's gender presentation: masculine or feminine.
@ -50,8 +69,9 @@ const typeDefs = gql`
id: ID! id: ID!
petStateId: ID! petStateId: ID!
bodyId: ID! bodyId: ID!
genderPresentation: GenderPresentation pose: Pose!
emotion: Emotion genderPresentation: GenderPresentation # deprecated
emotion: Emotion # deprecated
approximateThumbnailUrl: String! approximateThumbnailUrl: String!
layers: [AppearanceLayer!]! layers: [AppearanceLayer!]!
} }
@ -120,12 +140,7 @@ const typeDefs = gql`
offset: Int offset: Int
limit: Int limit: Int
): ItemSearchResult! ): ItemSearchResult!
petAppearance( petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance
speciesId: ID!
colorId: ID!
emotion: Emotion!
genderPresentation: GenderPresentation!
): PetAppearance
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]! petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
petOnNeopetsDotCom(petName: String!): Outfit petOnNeopetsDotCom(petName: String!): Outfit
@ -177,15 +192,15 @@ const resolvers = {
PetAppearance: { PetAppearance: {
id: ({ petType, petState }) => { id: ({ petType, petState }) => {
const { speciesId, colorId } = petType; const { speciesId, colorId } = petType;
const emotion = getEmotion(petState.moodId); const pose = getPoseFromPetState(petState);
const genderPresentation = getGenderPresentation(petState.female); return `${speciesId}-${colorId}-${pose}`;
return `${speciesId}-${colorId}-${emotion}-${genderPresentation}`;
}, },
petStateId: ({ petState }) => petState.id, petStateId: ({ petState }) => petState.id,
bodyId: ({ petType }) => petType.bodyId, bodyId: ({ petType }) => petType.bodyId,
pose: ({ petState }) => getPoseFromPetState(petState),
genderPresentation: ({ petState }) => genderPresentation: ({ petState }) =>
getGenderPresentation(petState.female), getGenderPresentation(getPoseFromPetState(petState)),
emotion: ({ petState }) => getEmotion(petState.moodId), emotion: ({ petState }) => getEmotion(getPoseFromPetState(petState)),
approximateThumbnailUrl: ({ petType, petState }) => { approximateThumbnailUrl: ({ petType, petState }) => {
return `http://pets.neopets.com/cp/${petType.basicImageHash}/${petState.moodId}/1.png`; return `http://pets.neopets.com/cp/${petType.basicImageHash}/${petState.moodId}/1.png`;
}, },
@ -309,7 +324,7 @@ const resolvers = {
}, },
petAppearance: async ( petAppearance: async (
_, _,
{ speciesId, colorId, emotion, genderPresentation }, { speciesId, colorId, pose },
{ petTypeLoader, petStateLoader } { petTypeLoader, petStateLoader }
) => { ) => {
const petType = await petTypeLoader.load({ const petType = await petTypeLoader.load({
@ -319,11 +334,7 @@ const resolvers = {
const petStates = await petStateLoader.load(petType.id); const petStates = await petStateLoader.load(petType.id);
// TODO: This could be optimized into the query condition 🤔 // TODO: This could be optimized into the query condition 🤔
const petState = petStates.find( const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
(ps) =>
getEmotion(ps.moodId) === emotion &&
getGenderPresentation(ps.female) === genderPresentation
);
if (!petState) { if (!petState) {
return null; return null;
} }

View file

@ -1,4 +1,5 @@
const DataLoader = require("dataloader"); const DataLoader = require("dataloader");
const { normalizeRow } = require("./util");
const loadAllColors = (db) => async () => { const loadAllColors = (db) => async () => {
const [rows, _] = await db.execute(`SELECT * FROM colors WHERE prank = 0`); const [rows, _] = await db.execute(`SELECT * FROM colors WHERE prank = 0`);
@ -277,18 +278,6 @@ const buildZoneTranslationLoader = (db) =>
); );
}); });
function normalizeRow(row) {
const normalizedRow = {};
for (let [key, value] of Object.entries(row)) {
key = key.replace(/_([a-z])/gi, (m) => m[1].toUpperCase());
if ((key === "id" || key.endsWith("Id")) && typeof value === "number") {
value = String(value);
}
normalizedRow[key] = value;
}
return normalizedRow;
}
function buildLoaders(db) { function buildLoaders(db) {
return { return {
loadAllColors: loadAllColors(db), loadAllColors: loadAllColors(db),

View file

@ -6,12 +6,7 @@ describe("PetAppearance", () => {
const res = await query({ const res = await query({
query: gql` query: gql`
query { query {
petAppearance( petAppearance(speciesId: "54", colorId: "75", pose: HAPPY_FEM) {
speciesId: "54"
colorId: "75"
emotion: HAPPY
genderPresentation: FEMININE
) {
layers { layers {
id id
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
@ -77,6 +72,7 @@ describe("PetAppearance", () => {
id id
bodyId bodyId
petStateId petStateId
pose
genderPresentation genderPresentation
emotion emotion
approximateThumbnailUrl approximateThumbnailUrl

View file

@ -64,8 +64,8 @@ Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "HAPPY", "emotion": "HAPPY",
"genderPresentation": "FEMININE", "genderPresentation": "MASCULINE",
"id": "54-75-HAPPY-FEMININE", "id": "54-75-HAPPY_FEM",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -111,13 +111,14 @@ Object {
}, },
], ],
"petStateId": "17723", "petStateId": "17723",
"pose": "HAPPY_FEM",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "HAPPY", "emotion": "HAPPY",
"genderPresentation": "MASCULINE", "genderPresentation": "MASCULINE",
"id": "54-75-HAPPY-MASCULINE", "id": "54-75-HAPPY_MASC",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -163,13 +164,14 @@ Object {
}, },
], ],
"petStateId": "17742", "petStateId": "17742",
"pose": "HAPPY_MASC",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SICK", "emotion": "SICK",
"genderPresentation": "FEMININE", "genderPresentation": "MASCULINE",
"id": "54-75-SICK-FEMININE", "id": "54-75-SICK_FEM",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -215,13 +217,14 @@ Object {
}, },
], ],
"petStateId": "10014", "petStateId": "10014",
"pose": "SICK_FEM",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SICK", "emotion": "SICK",
"genderPresentation": "MASCULINE", "genderPresentation": "MASCULINE",
"id": "54-75-SICK-MASCULINE", "id": "54-75-SICK_MASC",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -267,13 +270,14 @@ Object {
}, },
], ],
"petStateId": "11089", "petStateId": "11089",
"pose": "SICK_MASC",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SAD", "emotion": "SAD",
"genderPresentation": "FEMININE", "genderPresentation": "MASCULINE",
"id": "54-75-SAD-FEMININE", "id": "54-75-SAD_FEM",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -319,13 +323,14 @@ Object {
}, },
], ],
"petStateId": "5991", "petStateId": "5991",
"pose": "SAD_FEM",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SAD", "emotion": "SAD",
"genderPresentation": "MASCULINE", "genderPresentation": "MASCULINE",
"id": "54-75-SAD-MASCULINE", "id": "54-75-SAD_MASC",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -371,13 +376,14 @@ Object {
}, },
], ],
"petStateId": "436", "petStateId": "436",
"pose": "SAD_MASC",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": null, "emotion": null,
"genderPresentation": null, "genderPresentation": null,
"id": "54-75-null-null", "id": "54-75-UNKNOWN",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -430,13 +436,14 @@ Object {
}, },
], ],
"petStateId": "2", "petStateId": "2",
"pose": "UNKNOWN",
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png", "approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": null, "emotion": null,
"genderPresentation": null, "genderPresentation": null,
"id": "54-75-null-null", "id": "54-75-UNKNOWN",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -489,6 +496,7 @@ Object {
}, },
], ],
"petStateId": "4751", "petStateId": "4751",
"pose": "UNKNOWN",
}, },
], ],
} }

View file

@ -2,55 +2,77 @@ function capitalize(str) {
return str[0].toUpperCase() + str.slice(1); return str[0].toUpperCase() + str.slice(1);
} }
function getEmotion(moodId) { function getEmotion(pose) {
if (String(moodId) === "1") { if (["HAPPY_MASC", "HAPPY_FEM"].includes(pose)) {
return "HAPPY"; return "HAPPY";
} else if (String(moodId) === "2") { } else if (["SAD_MASC", "SAD_FEM"].includes(pose)) {
return "SAD"; return "SAD";
} else if (String(moodId) === "4") { } else if (["SICK_MASC", "SICK_FEM"].includes(pose)) {
return "SICK"; return "SICK";
} else if (moodId === null) { } else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
return null; return null;
} else { } else {
throw new Error(`unrecognized moodId ${JSON.stringify(moodId)}`); throw new Error(`unrecognized pose ${JSON.stringify(pose)}`);
} }
} }
function getGenderPresentation(modelPetWasFemale) { function getGenderPresentation(pose) {
if (String(modelPetWasFemale) === "1") { if (["HAPPY_MASC", "SAD_MASC", "SICK_MASC"].includes(pose)) {
return "FEMININE";
} else if (String(modelPetWasFemale) === "0") {
return "MASCULINE"; return "MASCULINE";
} else { } else if (["HAPPY_FEM", "SAD_FEM", "SICK_FEM"].includes(pose)) {
return "MASCULINE";
} else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
return null; return null;
} else {
throw new Error(`unrecognized pose ${JSON.stringify(pose)}`);
} }
} }
function getPose(moodId, modelPetWasFemale, isUnconverted) { function getPoseFromPetState(petState) {
if (isUnconverted) { const { moodId, female, unconverted } = petState;
if (unconverted) {
return "UNCONVERTED"; return "UNCONVERTED";
} else if (moodId == null || modelPetWasFemale == null) { } else if (moodId == null || female == null) {
return "UNKNOWN"; return "UNKNOWN";
} else if (String(moodId) === "1" && String(modelPetWasFemale) === "0") { } else if (String(moodId) === "1" && String(female) === "0") {
return "HAPPY_MASC"; return "HAPPY_MASC";
} else if (String(moodId) === "1" && String(modelPetWasFemale) === "1") { } else if (String(moodId) === "1" && String(female) === "1") {
return "HAPPY_FEM"; return "HAPPY_FEM";
} else if (String(moodId) === "2" && String(modelPetWasFemale) === "0") { } else if (String(moodId) === "2" && String(female) === "0") {
return "SAD_MASC"; return "SAD_MASC";
} else if (String(moodId) === "2" && String(modelPetWasFemale) === "1") { } else if (String(moodId) === "2" && String(female) === "1") {
return "SAD_FEM"; return "SAD_FEM";
} else if (String(moodId) === "4" && String(modelPetWasFemale) === "0") { } else if (String(moodId) === "4" && String(female) === "0") {
return "SICK_MASC"; return "SICK_MASC";
} else if (String(moodId) === "4" && String(modelPetWasFemale) === "1") { } else if (String(moodId) === "4" && String(female) === "1") {
return "SICK_FEM"; return "SICK_FEM";
} else { } else {
throw new Error( throw new Error(
`could not identify pose: ` + `could not identify pose: ` +
`moodId=${moodId}, ` + `moodId=${moodId}, ` +
`modelPetWasFemale=${modelPetWasFemale}, ` + `female=${female}, ` +
`isUnconverted=${isUnconverted}` `unconverted=${unconverted}`
); );
} }
} }
module.exports = { capitalize, getEmotion, getGenderPresentation, getPose }; function normalizeRow(row) {
const normalizedRow = {};
for (let [key, value] of Object.entries(row)) {
key = key.replace(/_([a-z])/gi, (m) => m[1].toUpperCase());
if ((key === "id" || key.endsWith("Id")) && typeof value === "number") {
value = String(value);
}
normalizedRow[key] = value;
}
return normalizedRow;
}
module.exports = {
capitalize,
getEmotion,
getGenderPresentation,
getPoseFromPetState,
normalizeRow,
};