server can read outfit data

This commit is contained in:
Matt Dunn-Rankin 2020-06-24 19:05:07 -07:00
parent ad947985ea
commit ade563ddcd
9 changed files with 531 additions and 90 deletions

View file

@ -1,3 +1,4 @@
-- Public data tables
GRANT SELECT ON openneo_impress.colors TO impress2020; GRANT SELECT ON openneo_impress.colors TO impress2020;
GRANT SELECT ON openneo_impress.color_translations TO impress2020; GRANT SELECT ON openneo_impress.color_translations TO impress2020;
GRANT SELECT ON openneo_impress.items TO impress2020; GRANT SELECT ON openneo_impress.items TO impress2020;
@ -10,3 +11,7 @@ GRANT SELECT ON openneo_impress.species_translations TO impress2020;
GRANT SELECT ON openneo_impress.swf_assets TO impress2020; GRANT SELECT ON openneo_impress.swf_assets TO impress2020;
GRANT SELECT ON openneo_impress.zones TO impress2020; GRANT SELECT ON openneo_impress.zones TO impress2020;
GRANT SELECT ON openneo_impress.zone_translations TO impress2020; GRANT SELECT ON openneo_impress.zone_translations TO impress2020;
-- User data tables
GRANT SELECT ON openneo_impress.item_outfit_relationships TO impress2020;
GRANT SELECT ON openneo_impress.outfits TO impress2020;

View file

@ -2341,7 +2341,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
10000000 10000000
00000000 00000000
00000000 00000000
00000000 10000000
10011011 10011011
00000000 00000000
10000000 10000000
@ -2555,7 +2555,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
00000000 00000000
10111111 10111111
00000000 00000000
00000000 10000000
10000000 10000000
00111111 00111111
00000000 00000000
@ -2797,7 +2797,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
10000000 10000000
10000000 10000000
10000000 10000000
00000000 10000000
00000000 00000000
00000000 00000000
10000000 10000000
@ -3936,7 +3936,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
10011011 10011011
00000000 00000000
10011011 10011011
00000000 10000000
00000000 00000000
00111111 00111111
00000000 00000000
@ -4575,7 +4575,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
00000000 00000000
00000000 00000000
00111111 00111111
00000000 10000000
00111111 00111111
10000000 10000000
10011011 10011011
@ -5937,7 +5937,7 @@ exports[`getValidPetPoses gets them and writes them to a buffer 1`] = `
10000000 10000000
00000000 00000000
00000000 00000000
00000000 10000000
00000000 00000000
00000000 00000000
00000000 00000000

View file

@ -70,13 +70,13 @@ const typeDefs = gql`
type PetAppearance { type PetAppearance {
id: ID! id: ID!
petStateId: ID! species: Species!
bodyId: ID! color: Color!
pose: Pose! pose: Pose!
genderPresentation: GenderPresentation # deprecated bodyId: ID!
emotion: Emotion # deprecated
approximateThumbnailUrl: String! # deprecated
layers: [AppearanceLayer!]! layers: [AppearanceLayer!]!
petStateId: ID! # Convenience field for developers
} }
type ItemAppearance { type ItemAppearance {
@ -125,10 +125,16 @@ const typeDefs = gql`
} }
type Outfit { type Outfit {
species: Species! id: ID!
color: Color! name: String!
pose: Pose! petAppearance: PetAppearance!
items: [Item!]! wornItems: [Item!]!
closetedItems: [Item!]!
species: Species! # to be deprecated? can use petAppearance? 🤔
color: Color! # to be deprecated? can use petAppearance? 🤔
pose: Pose! # to be deprecated? can use petAppearance? 🤔
items: [Item!]! # deprecated alias for wornItems
} }
type Query { type Query {
@ -147,6 +153,8 @@ const typeDefs = gql`
petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]! petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
outfit(id: ID!): Outfit
petOnNeopetsDotCom(petName: String!): Outfit petOnNeopetsDotCom(petName: String!): Outfit
} }
`; `;
@ -168,9 +176,9 @@ const resolvers = {
appearanceOn: async ( appearanceOn: async (
item, item,
{ speciesId, colorId }, { speciesId, colorId },
{ petTypeLoader, itemSwfAssetLoader } { petTypeBySpeciesAndColorLoader, itemSwfAssetLoader }
) => { ) => {
const petType = await petTypeLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId: speciesId, speciesId: speciesId,
colorId: colorId, colorId: colorId,
}); });
@ -199,22 +207,33 @@ const resolvers = {
}, },
}, },
PetAppearance: { PetAppearance: {
id: ({ petType, petState }) => { id: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const { speciesId, colorId } = petType; const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
const pose = getPoseFromPetState(petState); const pose = getPoseFromPetState(petState);
return `${speciesId}-${colorId}-${pose}`; return `${petType.speciesId}-${petType.colorId}-${pose}`;
}, },
petStateId: ({ petState }) => petState.id, color: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
bodyId: ({ petType }) => petType.bodyId, const petState = await petStateLoader.load(petStateId);
pose: ({ petState }) => getPoseFromPetState(petState), const petType = await petTypeLoader.load(petState.petTypeId);
genderPresentation: ({ petState }) => return { id: petType.colorId };
getGenderPresentation(getPoseFromPetState(petState)),
emotion: ({ petState }) => getEmotion(getPoseFromPetState(petState)),
approximateThumbnailUrl: ({ petType, petState }) => {
return `http://pets.neopets.com/cp/${petType.basicImageHash}/${petState.moodId}/1.png`;
}, },
layers: async ({ petState }, _, { petSwfAssetLoader }) => { species: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const swfAssets = await petSwfAssetLoader.load(petState.id); const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
return { id: petType.speciesId };
},
bodyId: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
return petType.bodyId;
},
pose: async ({ petStateId }, _, { petStateLoader }) => {
const petState = await petStateLoader.load(petStateId);
return getPoseFromPetState(petState);
},
layers: async ({ petStateId }, _, { petSwfAssetLoader }) => {
const swfAssets = await petSwfAssetLoader.load(petStateId);
return swfAssets; return swfAssets;
}, },
}, },
@ -285,19 +304,39 @@ const resolvers = {
}, },
}, },
Color: { Color: {
name: async (color, _, { colorTranslationLoader }) => { name: async ({ id }, _, { colorTranslationLoader }) => {
const colorTranslation = await colorTranslationLoader.load(color.id); const colorTranslation = await colorTranslationLoader.load(id);
return capitalize(colorTranslation.name); return capitalize(colorTranslation.name);
}, },
}, },
Species: { Species: {
name: async (species, _, { speciesTranslationLoader }) => { name: async ({ id }, _, { speciesTranslationLoader }) => {
const speciesTranslation = await speciesTranslationLoader.load( const speciesTranslation = await speciesTranslationLoader.load(id);
species.id
);
return capitalize(speciesTranslation.name); return capitalize(speciesTranslation.name);
}, },
}, },
Outfit: {
name: async ({ id }, _, { outfitLoader }) => {
const outfit = await outfitLoader.load(id);
return outfit.name;
},
petAppearance: async ({ id }, _, { outfitLoader }) => {
const outfit = await outfitLoader.load(id);
return { petStateId: outfit.petStateId };
},
wornItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
const relationships = await itemOutfitRelationshipsLoader.load(id);
return relationships
.filter((oir) => oir.isWorn)
.map((oir) => ({ id: oir.itemId }));
},
closetedItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
const relationships = await itemOutfitRelationshipsLoader.load(id);
return relationships
.filter((oir) => !oir.isWorn)
.map((oir) => ({ id: oir.itemId }));
},
},
Query: { Query: {
allColors: async (_, { ids }, { loadAllColors }) => { allColors: async (_, { ids }, { loadAllColors }) => {
const allColors = await loadAllColors(); const allColors = await loadAllColors();
@ -326,9 +365,12 @@ const resolvers = {
itemSearchToFit: async ( itemSearchToFit: async (
_, _,
{ query, speciesId, colorId, offset, limit }, { query, speciesId, colorId, offset, limit },
{ petTypeLoader, itemSearchToFitLoader } { petTypeBySpeciesAndColorLoader, itemSearchToFitLoader }
) => { ) => {
const petType = await petTypeLoader.load({ speciesId, colorId }); const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId,
colorId,
});
const { bodyId } = petType; const { bodyId } = petType;
const items = await itemSearchToFitLoader.load({ const items = await itemSearchToFitLoader.load({
query: query.trim(), query: query.trim(),
@ -341,40 +383,44 @@ const resolvers = {
petAppearance: async ( petAppearance: async (
_, _,
{ speciesId, colorId, pose }, { speciesId, colorId, pose },
{ petTypeLoader, petStateLoader } { petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
) => { ) => {
const petType = await petTypeLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId, speciesId,
colorId, colorId,
}); });
const petStates = await petStateLoader.load(petType.id); const petStates = await petStatesForPetTypeLoader.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((ps) => getPoseFromPetState(ps) === pose); const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
if (!petState) { if (!petState) {
return null; return null;
} }
return { petType, petState }; return { petStateId: petState.id };
}, },
petAppearances: async ( petAppearances: async (
_, _,
{ speciesId, colorId }, { speciesId, colorId },
{ petTypeLoader, petStateLoader } { petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
) => { ) => {
const petType = await petTypeLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId, speciesId,
colorId, colorId,
}); });
const petStates = await petStateLoader.load(petType.id); const petStates = await petStatesForPetTypeLoader.load(petType.id);
return petStates.map((petState) => ({ petType, petState })); return petStates.map((petState) => ({ petStateId: petState.id }));
}, },
outfit: (_, { id }) => ({ id }),
petOnNeopetsDotCom: async (_, { petName }) => { petOnNeopetsDotCom: async (_, { petName }) => {
const [petMetaData, customPetData] = await Promise.all([ const [petMetaData, customPetData] = await Promise.all([
neopets.loadPetMetaData(petName), neopets.loadPetMetaData(petName),
neopets.loadCustomPetData(petName), neopets.loadCustomPetData(petName),
]); ]);
const outfit = { const outfit = {
// TODO: This isn't a fully-working Outfit object. It works for the
// client as currently implemented, but we'll probably want to
// move the client and this onto our more generic fields!
species: { id: customPetData.custom_pet.species_id }, species: { id: customPetData.custom_pet.species_id },
color: { id: customPetData.custom_pet.color_id }, color: { id: customPetData.custom_pet.color_id },
pose: getPoseFromPetData(petMetaData, customPetData), pose: getPoseFromPetData(petMetaData, customPetData),

View file

@ -154,6 +154,21 @@ const buildItemSearchToFitLoader = (db) =>
}); });
const buildPetTypeLoader = (db) => const buildPetTypeLoader = (db) =>
new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM pet_types WHERE id IN (${qs})`,
petTypeIds
);
const entities = rows.map(normalizeRow);
return petTypeIds.map((petTypeId) =>
entities.find((e) => e.id === petTypeId)
);
});
const buildPetTypeBySpeciesAndColorLoader = (db, loaders) =>
new DataLoader(async (speciesAndColorPairs) => { new DataLoader(async (speciesAndColorPairs) => {
const conditions = []; const conditions = [];
const values = []; const values = [];
@ -172,6 +187,10 @@ const buildPetTypeLoader = (db) =>
entities.map((e) => [`${e.speciesId},${e.colorId}`, e]) entities.map((e) => [`${e.speciesId},${e.colorId}`, e])
); );
for (const petType of entities) {
loaders.petTypeLoader.prime(petType.id, petType);
}
return speciesAndColorPairs.map(({ speciesId, colorId }) => return speciesAndColorPairs.map(({ speciesId, colorId }) =>
entitiesBySpeciesAndColorPair.get(`${speciesId},${colorId}`) entitiesBySpeciesAndColorPair.get(`${speciesId},${colorId}`)
); );
@ -226,7 +245,50 @@ const buildPetSwfAssetLoader = (db) =>
); );
}); });
const buildOutfitLoader = (db) =>
new DataLoader(async (outfitIds) => {
const qs = outfitIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM outfits WHERE id IN (${qs})`,
outfitIds
);
const entities = rows.map(normalizeRow);
return outfitIds.map((outfitId) => entities.find((e) => e.id === outfitId));
});
const buildItemOutfitRelationshipsLoader = (db) =>
new DataLoader(async (outfitIds) => {
const qs = outfitIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM item_outfit_relationships WHERE outfit_id IN (${qs})`,
outfitIds
);
const entities = rows.map(normalizeRow);
return outfitIds.map((outfitId) =>
entities.filter((e) => e.outfitId === outfitId)
);
});
const buildPetStateLoader = (db) => const buildPetStateLoader = (db) =>
new DataLoader(async (petStateIds) => {
const qs = petStateIds.map((_) => "?").join(",");
const [rows, _] = await db.execute(
`SELECT * FROM pet_states WHERE id IN (${qs})`,
petStateIds
);
const entities = rows.map(normalizeRow);
return petStateIds.map((petStateId) =>
entities.find((e) => e.id === petStateId)
);
});
const buildPetStatesForPetTypeLoader = (db, loaders) =>
new DataLoader(async (petTypeIds) => { new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(","); const qs = petTypeIds.map((_) => "?").join(",");
const [rows, _] = await db.execute( const [rows, _] = await db.execute(
@ -238,6 +300,10 @@ const buildPetStateLoader = (db) =>
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
for (const petState of entities) {
loaders.petStateLoader.prime(petState.id, petState);
}
return petTypeIds.map((petTypeId) => return petTypeIds.map((petTypeId) =>
entities.filter((e) => e.petTypeId === petTypeId) entities.filter((e) => e.petTypeId === petTypeId)
); );
@ -280,24 +346,37 @@ const buildZoneTranslationLoader = (db) =>
}); });
function buildLoaders(db) { function buildLoaders(db) {
return { const loaders = {};
loadAllColors: loadAllColors(db), loaders.loadAllColors = loadAllColors(db);
loadAllSpecies: loadAllSpecies(db), loaders.loadAllSpecies = loadAllSpecies(db);
loadAllPetTypes: loadAllPetTypes(db), loaders.loadAllPetTypes = loadAllPetTypes(db);
colorTranslationLoader: buildColorTranslationLoader(db), loaders.colorTranslationLoader = buildColorTranslationLoader(db);
itemLoader: buildItemsLoader(db), loaders.itemLoader = buildItemsLoader(db);
itemTranslationLoader: buildItemTranslationLoader(db), loaders.itemTranslationLoader = buildItemTranslationLoader(db);
itemSearchLoader: buildItemSearchLoader(db), loaders.itemSearchLoader = buildItemSearchLoader(db);
itemSearchToFitLoader: buildItemSearchToFitLoader(db), loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db);
petTypeLoader: buildPetTypeLoader(db), loaders.petTypeLoader = buildPetTypeLoader(db);
itemSwfAssetLoader: buildItemSwfAssetLoader(db), loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
petSwfAssetLoader: buildPetSwfAssetLoader(db), db,
petStateLoader: buildPetStateLoader(db), loaders
speciesTranslationLoader: buildSpeciesTranslationLoader(db), );
zoneLoader: buildZoneLoader(db), loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db);
zoneTranslationLoader: buildZoneTranslationLoader(db), loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db);
}; loaders.outfitLoader = buildOutfitLoader(db);
loaders.itemOutfitRelationshipsLoader = buildItemOutfitRelationshipsLoader(
db
);
loaders.petStateLoader = buildPetStateLoader(db);
loaders.petStatesForPetTypeLoader = buildPetStatesForPetTypeLoader(
db,
loaders
);
loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db);
loaders.zoneLoader = buildZoneLoader(db);
loaders.zoneTranslationLoader = buildZoneTranslationLoader(db);
return loaders;
} }
module.exports = buildLoaders; module.exports = buildLoaders;

View file

@ -0,0 +1,104 @@
const gql = require("graphql-tag");
const { query, getDbCalls } = require("./setup.js");
describe("Outfit", () => {
it("loads an outfit by ID", async () => {
const res = await query({
query: gql`
query {
outfit(id: "31856") {
id
name
petAppearance {
id
color {
id
name
}
species {
id
name
}
pose
}
wornItems {
id
name
}
closetedItems {
id
name
}
}
}
`,
});
expect(res).toHaveNoErrors();
expect(res.data).toMatchSnapshot();
expect(getDbCalls()).toMatchInlineSnapshot(`
Array [
Array [
"SELECT * FROM outfits WHERE id IN (?)",
Array [
"31856",
],
],
Array [
"SELECT * FROM item_outfit_relationships WHERE outfit_id IN (?)",
Array [
"31856",
],
],
Array [
"SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?,?,?,?) AND locale = \\"en\\"",
Array [
"38916",
"51054",
"38914",
"36125",
"36467",
"47075",
"47056",
"39662",
"56706",
"38915",
"56398",
],
],
Array [
"SELECT * FROM pet_states WHERE id IN (?)",
Array [
"3951",
],
],
Array [
"SELECT * FROM pet_types WHERE id IN (?)",
Array [
"33",
],
],
Array [
"SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"",
Array [
"34",
],
],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"54",
],
],
]
`);
});
});

View file

@ -7,6 +7,18 @@ describe("PetAppearance", () => {
query: gql` query: gql`
query { query {
petAppearance(speciesId: "54", colorId: "75", pose: HAPPY_FEM) { petAppearance(speciesId: "54", colorId: "75", pose: HAPPY_FEM) {
id
species {
id
name
}
color {
id
name
}
layers { layers {
id id
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
@ -49,6 +61,20 @@ describe("PetAppearance", () => {
"17723", "17723",
], ],
], ],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"54",
],
],
Array [
"SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"",
Array [
"75",
],
],
Array [ Array [
"SELECT * FROM zones WHERE id IN (?,?,?,?,?,?)", "SELECT * FROM zones WHERE id IN (?,?,?,?,?,?)",
Array [ Array [
@ -70,12 +96,20 @@ describe("PetAppearance", () => {
query { query {
petAppearances(speciesId: "54", colorId: "75") { petAppearances(speciesId: "54", colorId: "75") {
id id
species {
id
name
}
color {
id
name
}
bodyId bodyId
petStateId petStateId
pose pose
genderPresentation
emotion
approximateThumbnailUrl
layers { layers {
id id
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
@ -124,6 +158,20 @@ describe("PetAppearance", () => {
"4751", "4751",
], ],
], ],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"54",
],
],
Array [
"SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"",
Array [
"75",
],
],
Array [ Array [
"SELECT * FROM zones WHERE id IN (?,?,?,?,?,?)", "SELECT * FROM zones WHERE id IN (?,?,?,?,?,?)",
Array [ Array [

View file

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Outfit loads an outfit by ID 1`] = `
Object {
"outfit": Object {
"closetedItems": Array [
Object {
"id": "36125",
"name": "Blue Newsboy Hat",
},
Object {
"id": "36467",
"name": "Daring Adventurer Hat",
},
Object {
"id": "47075",
"name": "Jordies Adventure Hat",
},
Object {
"id": "47056",
"name": "Moltara Inventor Hat and Goggles",
},
Object {
"id": "39662",
"name": "Simple Sun Hat",
},
Object {
"id": "56706",
"name": "Super Sleuth Hat and Wig",
},
Object {
"id": "38915",
"name": "Zafara Tourist Shirt",
},
Object {
"id": "56398",
"name": "Altador Cup Kreludor Frame",
},
],
"id": "31856",
"name": "Zafara Tourist",
"petAppearance": Object {
"color": Object {
"id": "34",
"name": "Green",
},
"id": "54-34-UNKNOWN",
"pose": "UNKNOWN",
"species": Object {
"id": "54",
"name": "Zafara",
},
},
"wornItems": Array [
Object {
"id": "38916",
"name": "Zafara Tourist Camera",
},
Object {
"id": "51054",
"name": "Summer Fun Beach Background",
},
Object {
"id": "38914",
"name": "Zafara Tourist Hat",
},
],
},
}
`;

View file

@ -3,6 +3,11 @@
exports[`PetAppearance loads for species and color 1`] = ` exports[`PetAppearance loads for species and color 1`] = `
Object { Object {
"petAppearance": Object { "petAppearance": Object {
"color": Object {
"id": "75",
"name": "Starry",
},
"id": "54-75-HAPPY_FEM",
"layers": Array [ "layers": Array [
Object { Object {
"id": "5995", "id": "5995",
@ -53,6 +58,10 @@ Object {
}, },
}, },
], ],
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
} }
`; `;
@ -61,10 +70,11 @@ exports[`PetAppearance loads multiple for species and color 1`] = `
Object { Object {
"petAppearances": Array [ "petAppearances": Array [
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "HAPPY", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-HAPPY_FEM", "id": "54-75-HAPPY_FEM",
"layers": Array [ "layers": Array [
Object { Object {
@ -112,12 +122,17 @@ Object {
], ],
"petStateId": "17723", "petStateId": "17723",
"pose": "HAPPY_FEM", "pose": "HAPPY_FEM",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/1/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "HAPPY", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-HAPPY_MASC", "id": "54-75-HAPPY_MASC",
"layers": Array [ "layers": Array [
Object { Object {
@ -165,12 +180,17 @@ Object {
], ],
"petStateId": "17742", "petStateId": "17742",
"pose": "HAPPY_MASC", "pose": "HAPPY_MASC",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SICK", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-SICK_FEM", "id": "54-75-SICK_FEM",
"layers": Array [ "layers": Array [
Object { Object {
@ -218,12 +238,17 @@ Object {
], ],
"petStateId": "10014", "petStateId": "10014",
"pose": "SICK_FEM", "pose": "SICK_FEM",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/4/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SICK", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-SICK_MASC", "id": "54-75-SICK_MASC",
"layers": Array [ "layers": Array [
Object { Object {
@ -271,12 +296,17 @@ Object {
], ],
"petStateId": "11089", "petStateId": "11089",
"pose": "SICK_MASC", "pose": "SICK_MASC",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SAD", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-SAD_FEM", "id": "54-75-SAD_FEM",
"layers": Array [ "layers": Array [
Object { Object {
@ -324,12 +354,17 @@ Object {
], ],
"petStateId": "5991", "petStateId": "5991",
"pose": "SAD_FEM", "pose": "SAD_FEM",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/2/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": "SAD", "color": Object {
"genderPresentation": "MASCULINE", "id": "75",
"name": "Starry",
},
"id": "54-75-SAD_MASC", "id": "54-75-SAD_MASC",
"layers": Array [ "layers": Array [
Object { Object {
@ -377,12 +412,17 @@ Object {
], ],
"petStateId": "436", "petStateId": "436",
"pose": "SAD_MASC", "pose": "SAD_MASC",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": null, "color": Object {
"genderPresentation": null, "id": "75",
"name": "Starry",
},
"id": "54-75-UNKNOWN", "id": "54-75-UNKNOWN",
"layers": Array [ "layers": Array [
Object { Object {
@ -437,12 +477,17 @@ Object {
], ],
"petStateId": "2", "petStateId": "2",
"pose": "UNKNOWN", "pose": "UNKNOWN",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
Object { Object {
"approximateThumbnailUrl": "http://pets.neopets.com/cp/vghhzlgf/null/1.png",
"bodyId": "180", "bodyId": "180",
"emotion": null, "color": Object {
"genderPresentation": null, "id": "75",
"name": "Starry",
},
"id": "54-75-UNKNOWN", "id": "54-75-UNKNOWN",
"layers": Array [ "layers": Array [
Object { Object {
@ -497,6 +542,10 @@ Object {
], ],
"petStateId": "4751", "petStateId": "4751",
"pose": "UNKNOWN", "pose": "UNKNOWN",
"species": Object {
"id": "54",
"name": "Zafara",
},
}, },
], ],
} }

View file

@ -11235,6 +11235,14 @@ Object {
"id": "21", "id": "21",
}, },
}, },
Object {
"color": Object {
"id": "99",
},
"species": Object {
"id": "21",
},
},
Object { Object {
"color": Object { "color": Object {
"id": "100", "id": "100",
@ -12243,6 +12251,14 @@ Object {
"id": "23", "id": "23",
}, },
}, },
Object {
"color": Object {
"id": "89",
},
"species": Object {
"id": "23",
},
},
Object { Object {
"color": Object { "color": Object {
"id": "90", "id": "90",
@ -18635,6 +18651,14 @@ Object {
"id": "36", "id": "36",
}, },
}, },
Object {
"color": Object {
"id": "14",
},
"species": Object {
"id": "36",
},
},
Object { Object {
"color": Object { "color": Object {
"id": "16", "id": "16",
@ -21523,6 +21547,14 @@ Object {
"id": "41", "id": "41",
}, },
}, },
Object {
"color": Object {
"id": "93",
},
"species": Object {
"id": "41",
},
},
Object { Object {
"color": Object { "color": Object {
"id": "94", "id": "94",
@ -27859,6 +27891,14 @@ Object {
"id": "53", "id": "53",
}, },
}, },
Object {
"color": Object {
"id": "111",
},
"species": Object {
"id": "53",
},
},
Object { Object {
"color": Object { "color": Object {
"id": "6", "id": "6",