cache item data when switching standard colors
Previously, when changing a pet's color, we would refresh the items panel and send a new network request for the item appearances, even though they're all the same. This is because item appearance data is queried by species/color, for ease of specification. But! Item appearances are //cached// by body ID. So, if this is a standard color, it's not hard to look in the cache for the standard color's body ID! Now, most color changes are faster and don't flicker the item panel anymore. We do still refresh the panel and send the requests for color changes that _do_ matter though, like standard <-> mutant!
This commit is contained in:
parent
4a91d0cab8
commit
856d8586e4
7 changed files with 402 additions and 16 deletions
|
@ -1,30 +1,96 @@
|
|||
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
const cachedZones = require("./cached-data/zones.json");
|
||||
const cachedZonesById = new Map(cachedZones.map((z) => [z.id, z]));
|
||||
|
||||
// Teach Apollo to load certain fields from the cache, to avoid extra network
|
||||
// requests. This happens a lot - e.g. reusing data from item search on the
|
||||
// outfit immediately!
|
||||
const typePolicies = {
|
||||
Query: {
|
||||
fields: {
|
||||
// Teach Apollo how to serve `items` queries from the cache. That way,
|
||||
// when you remove an item from your outfit, or add an item from search,
|
||||
// Apollo knows it already has the data it needs and doesn't need to ask
|
||||
// the server again!
|
||||
items: (_, { args, toReference }) => {
|
||||
return args.ids.map((id) =>
|
||||
toReference({ __typename: "Item", id }, true)
|
||||
);
|
||||
},
|
||||
|
||||
// Similar for a single item lookup!
|
||||
item: (_, { args, toReference }) => {
|
||||
return toReference({ __typename: "Item", id: args.id }, true);
|
||||
},
|
||||
|
||||
petAppearanceById: (_, { args, toReference }) => {
|
||||
return toReference({ __typename: "PetAppearance", id: args.id }, true);
|
||||
},
|
||||
species: (_, { args, toReference }) => {
|
||||
return toReference({ __typename: "Species", id: args.id }, true);
|
||||
},
|
||||
color: (_, { args, toReference }) => {
|
||||
return toReference({ __typename: "Color", id: args.id }, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Item: {
|
||||
fields: {
|
||||
appearanceOn: (appearance, { args, readField, toReference }) => {
|
||||
// If we already have this exact appearance in the cache, serve it!
|
||||
if (appearance) {
|
||||
return appearance;
|
||||
}
|
||||
|
||||
// Otherwise, we're going to see if this is a standard color, in which
|
||||
// case we can reuse the standard color appearance if we already have
|
||||
// it! This helps for fast loading when switching between standard
|
||||
// colors.
|
||||
|
||||
const { speciesId, colorId } = args;
|
||||
|
||||
// HACK: I can't find a way to do bigger-picture queries like this from
|
||||
// Apollo's cache field reader API. Am I missing something? I
|
||||
// don't love escape-hatching to the client like this, but...
|
||||
let cachedData;
|
||||
try {
|
||||
cachedData = client.readQuery({
|
||||
query: gql`
|
||||
query CacheLookupForItemAppearanceReader(
|
||||
$speciesId: ID!
|
||||
$colorId: ID!
|
||||
) {
|
||||
species(id: $speciesId) {
|
||||
standardBodyId
|
||||
}
|
||||
color(id: $colorId) {
|
||||
isStandard
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { speciesId, colorId },
|
||||
});
|
||||
} catch (e) {
|
||||
// Some errors are expected while setting up the cache... not sure
|
||||
// how to distinguish from Real errors. Just gonna ignore them all
|
||||
// for now!
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!cachedData) {
|
||||
// This is an expected case while the page is loading.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { species, color } = cachedData;
|
||||
if (color.isStandard) {
|
||||
const itemId = readField("id");
|
||||
const bodyId = species.standardBodyId;
|
||||
return toReference({
|
||||
__typename: "ItemAppearance",
|
||||
id: `item-${itemId}-body-${bodyId}`,
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -54,8 +120,10 @@ const httpLink = createHttpLink({ uri: "/api/graphql" });
|
|||
* apolloClient is the global Apollo Client instance we use for GraphQL
|
||||
* queries. This is how we communicate with the server!
|
||||
*/
|
||||
export default new ApolloClient({
|
||||
const client = new ApolloClient({
|
||||
link: persistedQueryLink.concat(httpLink),
|
||||
cache: new InMemoryCache({ typePolicies }),
|
||||
connectToDevTools: true,
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
|
|
@ -32,12 +32,13 @@ function SpeciesColorPicker({
|
|||
allSpecies {
|
||||
id
|
||||
name
|
||||
standardBodyId # Used for keeping items on during standard color changes
|
||||
}
|
||||
|
||||
allColors {
|
||||
id
|
||||
name
|
||||
isStandard # Not used here, but helpful for caching!
|
||||
isStandard # Used for keeping items on during standard color changes
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
@ -182,6 +182,11 @@ const typeDefs = gql`
|
|||
type Species @cacheControl(maxAge: 604800) {
|
||||
id: ID!
|
||||
name: String!
|
||||
|
||||
# The bodyId for PetAppearances that use this species and a standard color.
|
||||
# We use this to preload the standard body IDs, so that items stay when
|
||||
# switching between standard colors.
|
||||
standardBodyId: ID!
|
||||
}
|
||||
|
||||
type SpeciesColorPair {
|
||||
|
@ -231,6 +236,9 @@ const typeDefs = gql`
|
|||
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
|
||||
outfit(id: ID!): Outfit
|
||||
|
||||
color(id: ID!): Color
|
||||
species(id: ID!): Species
|
||||
|
||||
petOnNeopetsDotCom(petName: String!): Outfit
|
||||
}
|
||||
|
||||
|
@ -523,6 +531,13 @@ const resolvers = {
|
|||
const speciesTranslation = await speciesTranslationLoader.load(id);
|
||||
return capitalize(speciesTranslation.name);
|
||||
},
|
||||
standardBodyId: async ({ id }, _, { petTypeBySpeciesAndColorLoader }) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId: id,
|
||||
colorId: "8", // Blue
|
||||
});
|
||||
return petType.bodyId;
|
||||
},
|
||||
},
|
||||
Outfit: {
|
||||
name: async ({ id }, _, { outfitLoader }) => {
|
||||
|
@ -551,8 +566,8 @@ const resolvers = {
|
|||
const allColors = await colorLoader.loadAll();
|
||||
return allColors;
|
||||
},
|
||||
allSpecies: async (_, { ids }, { loadAllSpecies }) => {
|
||||
const allSpecies = await loadAllSpecies();
|
||||
allSpecies: async (_, { ids }, { speciesLoader }) => {
|
||||
const allSpecies = await speciesLoader.loadAll();
|
||||
return allSpecies;
|
||||
},
|
||||
allValidSpeciesColorPairs: async (_, __, { loadAllPetTypes }) => {
|
||||
|
@ -645,6 +660,20 @@ const resolvers = {
|
|||
};
|
||||
return outfit;
|
||||
},
|
||||
color: async (_, { id }, { colorLoader }) => {
|
||||
const color = await colorLoader.load(id);
|
||||
if (!color) {
|
||||
return null;
|
||||
}
|
||||
return { id };
|
||||
},
|
||||
species: async (_, { id }, { speciesLoader }) => {
|
||||
const species = await speciesLoader.load(id);
|
||||
if (!species) {
|
||||
return null;
|
||||
}
|
||||
return { id };
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
setManualSpecialColor: async (
|
||||
|
|
|
@ -52,10 +52,36 @@ const buildColorTranslationLoader = (db) =>
|
|||
);
|
||||
});
|
||||
|
||||
const loadAllSpecies = (db) => async () => {
|
||||
const [rows, _] = await db.execute(`SELECT * FROM species`);
|
||||
const entities = rows.map(normalizeRow);
|
||||
return entities;
|
||||
const buildSpeciesLoader = (db) => {
|
||||
const speciesLoader = new DataLoader(async (speciesIds) => {
|
||||
const qs = speciesIds.map((_) => "?").join(",");
|
||||
const [rows, _] = await db.execute(
|
||||
`SELECT * FROM species WHERE id IN (${qs})`,
|
||||
speciesIds
|
||||
);
|
||||
|
||||
const entities = rows.map(normalizeRow);
|
||||
const entitiesBySpeciesId = new Map(entities.map((e) => [e.id, e]));
|
||||
|
||||
return speciesIds.map(
|
||||
(speciesId) =>
|
||||
entitiesBySpeciesId.get(String(speciesId)) ||
|
||||
new Error(`could not find color ${speciesId}`)
|
||||
);
|
||||
});
|
||||
|
||||
speciesLoader.loadAll = async () => {
|
||||
const [rows, _] = await db.execute(`SELECT * FROM species`);
|
||||
const entities = rows.map(normalizeRow);
|
||||
|
||||
for (const species of entities) {
|
||||
speciesLoader.prime(species.id, species);
|
||||
}
|
||||
|
||||
return entities;
|
||||
};
|
||||
|
||||
return speciesLoader;
|
||||
};
|
||||
|
||||
const buildSpeciesTranslationLoader = (db) =>
|
||||
|
@ -409,7 +435,6 @@ const buildZoneTranslationLoader = (db) =>
|
|||
|
||||
function buildLoaders(db) {
|
||||
const loaders = {};
|
||||
loaders.loadAllSpecies = loadAllSpecies(db);
|
||||
loaders.loadAllPetTypes = loadAllPetTypes(db);
|
||||
|
||||
loaders.colorLoader = buildColorLoader(db);
|
||||
|
@ -435,6 +460,7 @@ function buildLoaders(db) {
|
|||
db,
|
||||
loaders
|
||||
);
|
||||
loaders.speciesLoader = buildSpeciesLoader(db);
|
||||
loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db);
|
||||
loaders.zoneLoader = buildZoneLoader(db);
|
||||
loaders.zoneTranslationLoader = buildZoneTranslationLoader(db);
|
||||
|
|
|
@ -2,6 +2,48 @@ const gql = require("graphql-tag");
|
|||
const { query, getDbCalls } = require("./setup.js");
|
||||
|
||||
describe("Color", () => {
|
||||
it("loads a single color", async () => {
|
||||
const res = await query({
|
||||
query: gql`
|
||||
query {
|
||||
color(id: "8") {
|
||||
id
|
||||
name
|
||||
isStandard
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(res).toHaveNoErrors();
|
||||
expect(res.data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"color": Object {
|
||||
"id": "8",
|
||||
"isStandard": true,
|
||||
"name": "Blue",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getDbCalls()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"SELECT * FROM colors WHERE id IN (?) AND prank = 0",
|
||||
Array [
|
||||
"8",
|
||||
],
|
||||
],
|
||||
Array [
|
||||
"SELECT * FROM color_translations
|
||||
WHERE color_id IN (?) AND locale = \\"en\\"",
|
||||
Array [
|
||||
"8",
|
||||
],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("loads all colors", async () => {
|
||||
const res = await query({
|
||||
query: gql`
|
||||
|
|
|
@ -2,6 +2,55 @@ const gql = require("graphql-tag");
|
|||
const { query, getDbCalls } = require("./setup.js");
|
||||
|
||||
describe("Species", () => {
|
||||
it("loads a single species", async () => {
|
||||
const res = await query({
|
||||
query: gql`
|
||||
query {
|
||||
species(id: "1") {
|
||||
id
|
||||
name
|
||||
standardBodyId
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(res).toHaveNoErrors();
|
||||
expect(res.data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"species": Object {
|
||||
"id": "1",
|
||||
"name": "Acara",
|
||||
"standardBodyId": "93",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getDbCalls()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"SELECT * FROM species WHERE id IN (?)",
|
||||
Array [
|
||||
"1",
|
||||
],
|
||||
],
|
||||
Array [
|
||||
"SELECT * FROM species_translations
|
||||
WHERE species_id IN (?) AND locale = \\"en\\"",
|
||||
Array [
|
||||
"1",
|
||||
],
|
||||
],
|
||||
Array [
|
||||
"SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)",
|
||||
Array [
|
||||
"1",
|
||||
"8",
|
||||
],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("loads all species", async () => {
|
||||
const res = await query({
|
||||
query: gql`
|
||||
|
@ -9,6 +58,7 @@ describe("Species", () => {
|
|||
allSpecies {
|
||||
id
|
||||
name
|
||||
standardBodyId
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
@ -82,6 +132,121 @@ describe("Species", () => {
|
|||
"55",
|
||||
],
|
||||
],
|
||||
Array [
|
||||
"SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?) OR (species_id = ? AND color_id = ?)",
|
||||
Array [
|
||||
"1",
|
||||
"8",
|
||||
"2",
|
||||
"8",
|
||||
"3",
|
||||
"8",
|
||||
"4",
|
||||
"8",
|
||||
"5",
|
||||
"8",
|
||||
"6",
|
||||
"8",
|
||||
"7",
|
||||
"8",
|
||||
"8",
|
||||
"8",
|
||||
"9",
|
||||
"8",
|
||||
"10",
|
||||
"8",
|
||||
"11",
|
||||
"8",
|
||||
"12",
|
||||
"8",
|
||||
"13",
|
||||
"8",
|
||||
"14",
|
||||
"8",
|
||||
"15",
|
||||
"8",
|
||||
"16",
|
||||
"8",
|
||||
"17",
|
||||
"8",
|
||||
"18",
|
||||
"8",
|
||||
"19",
|
||||
"8",
|
||||
"20",
|
||||
"8",
|
||||
"21",
|
||||
"8",
|
||||
"22",
|
||||
"8",
|
||||
"23",
|
||||
"8",
|
||||
"24",
|
||||
"8",
|
||||
"25",
|
||||
"8",
|
||||
"26",
|
||||
"8",
|
||||
"27",
|
||||
"8",
|
||||
"28",
|
||||
"8",
|
||||
"29",
|
||||
"8",
|
||||
"30",
|
||||
"8",
|
||||
"31",
|
||||
"8",
|
||||
"32",
|
||||
"8",
|
||||
"33",
|
||||
"8",
|
||||
"34",
|
||||
"8",
|
||||
"35",
|
||||
"8",
|
||||
"36",
|
||||
"8",
|
||||
"37",
|
||||
"8",
|
||||
"38",
|
||||
"8",
|
||||
"39",
|
||||
"8",
|
||||
"40",
|
||||
"8",
|
||||
"41",
|
||||
"8",
|
||||
"42",
|
||||
"8",
|
||||
"43",
|
||||
"8",
|
||||
"44",
|
||||
"8",
|
||||
"45",
|
||||
"8",
|
||||
"46",
|
||||
"8",
|
||||
"47",
|
||||
"8",
|
||||
"48",
|
||||
"8",
|
||||
"49",
|
||||
"8",
|
||||
"50",
|
||||
"8",
|
||||
"51",
|
||||
"8",
|
||||
"52",
|
||||
"8",
|
||||
"53",
|
||||
"8",
|
||||
"54",
|
||||
"8",
|
||||
"55",
|
||||
"8",
|
||||
],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -6,222 +6,277 @@ Object {
|
|||
Object {
|
||||
"id": "1",
|
||||
"name": "Acara",
|
||||
"standardBodyId": "93",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "Aisha",
|
||||
"standardBodyId": "106",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "Blumaroo",
|
||||
"standardBodyId": "47",
|
||||
},
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "Bori",
|
||||
"standardBodyId": "84",
|
||||
},
|
||||
Object {
|
||||
"id": "5",
|
||||
"name": "Bruce",
|
||||
"standardBodyId": "146",
|
||||
},
|
||||
Object {
|
||||
"id": "6",
|
||||
"name": "Buzz",
|
||||
"standardBodyId": "250",
|
||||
},
|
||||
Object {
|
||||
"id": "7",
|
||||
"name": "Chia",
|
||||
"standardBodyId": "212",
|
||||
},
|
||||
Object {
|
||||
"id": "8",
|
||||
"name": "Chomby",
|
||||
"standardBodyId": "74",
|
||||
},
|
||||
Object {
|
||||
"id": "9",
|
||||
"name": "Cybunny",
|
||||
"standardBodyId": "94",
|
||||
},
|
||||
Object {
|
||||
"id": "10",
|
||||
"name": "Draik",
|
||||
"standardBodyId": "132",
|
||||
},
|
||||
Object {
|
||||
"id": "11",
|
||||
"name": "Elephante",
|
||||
"standardBodyId": "56",
|
||||
},
|
||||
Object {
|
||||
"id": "12",
|
||||
"name": "Eyrie",
|
||||
"standardBodyId": "90",
|
||||
},
|
||||
Object {
|
||||
"id": "13",
|
||||
"name": "Flotsam",
|
||||
"standardBodyId": "136",
|
||||
},
|
||||
Object {
|
||||
"id": "14",
|
||||
"name": "Gelert",
|
||||
"standardBodyId": "138",
|
||||
},
|
||||
Object {
|
||||
"id": "15",
|
||||
"name": "Gnorbu",
|
||||
"standardBodyId": "166",
|
||||
},
|
||||
Object {
|
||||
"id": "16",
|
||||
"name": "Grarrl",
|
||||
"standardBodyId": "119",
|
||||
},
|
||||
Object {
|
||||
"id": "17",
|
||||
"name": "Grundo",
|
||||
"standardBodyId": "126",
|
||||
},
|
||||
Object {
|
||||
"id": "18",
|
||||
"name": "Hissi",
|
||||
"standardBodyId": "67",
|
||||
},
|
||||
Object {
|
||||
"id": "19",
|
||||
"name": "Ixi",
|
||||
"standardBodyId": "163",
|
||||
},
|
||||
Object {
|
||||
"id": "20",
|
||||
"name": "Jetsam",
|
||||
"standardBodyId": "147",
|
||||
},
|
||||
Object {
|
||||
"id": "21",
|
||||
"name": "Jubjub",
|
||||
"standardBodyId": "80",
|
||||
},
|
||||
Object {
|
||||
"id": "22",
|
||||
"name": "Kacheek",
|
||||
"standardBodyId": "117",
|
||||
},
|
||||
Object {
|
||||
"id": "23",
|
||||
"name": "Kau",
|
||||
"standardBodyId": "201",
|
||||
},
|
||||
Object {
|
||||
"id": "24",
|
||||
"name": "Kiko",
|
||||
"standardBodyId": "51",
|
||||
},
|
||||
Object {
|
||||
"id": "25",
|
||||
"name": "Koi",
|
||||
"standardBodyId": "208",
|
||||
},
|
||||
Object {
|
||||
"id": "26",
|
||||
"name": "Korbat",
|
||||
"standardBodyId": "196",
|
||||
},
|
||||
Object {
|
||||
"id": "27",
|
||||
"name": "Kougra",
|
||||
"standardBodyId": "143",
|
||||
},
|
||||
Object {
|
||||
"id": "28",
|
||||
"name": "Krawk",
|
||||
"standardBodyId": "150",
|
||||
},
|
||||
Object {
|
||||
"id": "29",
|
||||
"name": "Kyrii",
|
||||
"standardBodyId": "175",
|
||||
},
|
||||
Object {
|
||||
"id": "30",
|
||||
"name": "Lenny",
|
||||
"standardBodyId": "173",
|
||||
},
|
||||
Object {
|
||||
"id": "31",
|
||||
"name": "Lupe",
|
||||
"standardBodyId": "199",
|
||||
},
|
||||
Object {
|
||||
"id": "32",
|
||||
"name": "Lutari",
|
||||
"standardBodyId": "52",
|
||||
},
|
||||
Object {
|
||||
"id": "33",
|
||||
"name": "Meerca",
|
||||
"standardBodyId": "109",
|
||||
},
|
||||
Object {
|
||||
"id": "34",
|
||||
"name": "Moehog",
|
||||
"standardBodyId": "134",
|
||||
},
|
||||
Object {
|
||||
"id": "35",
|
||||
"name": "Mynci",
|
||||
"standardBodyId": "95",
|
||||
},
|
||||
Object {
|
||||
"id": "36",
|
||||
"name": "Nimmo",
|
||||
"standardBodyId": "96",
|
||||
},
|
||||
Object {
|
||||
"id": "37",
|
||||
"name": "Ogrin",
|
||||
"standardBodyId": "154",
|
||||
},
|
||||
Object {
|
||||
"id": "38",
|
||||
"name": "Peophin",
|
||||
"standardBodyId": "55",
|
||||
},
|
||||
Object {
|
||||
"id": "39",
|
||||
"name": "Poogle",
|
||||
"standardBodyId": "76",
|
||||
},
|
||||
Object {
|
||||
"id": "40",
|
||||
"name": "Pteri",
|
||||
"standardBodyId": "156",
|
||||
},
|
||||
Object {
|
||||
"id": "41",
|
||||
"name": "Quiggle",
|
||||
"standardBodyId": "78",
|
||||
},
|
||||
Object {
|
||||
"id": "42",
|
||||
"name": "Ruki",
|
||||
"standardBodyId": "191",
|
||||
},
|
||||
Object {
|
||||
"id": "43",
|
||||
"name": "Scorchio",
|
||||
"standardBodyId": "187",
|
||||
},
|
||||
Object {
|
||||
"id": "44",
|
||||
"name": "Shoyru",
|
||||
"standardBodyId": "46",
|
||||
},
|
||||
Object {
|
||||
"id": "45",
|
||||
"name": "Skeith",
|
||||
"standardBodyId": "178",
|
||||
},
|
||||
Object {
|
||||
"id": "46",
|
||||
"name": "Techo",
|
||||
"standardBodyId": "100",
|
||||
},
|
||||
Object {
|
||||
"id": "47",
|
||||
"name": "Tonu",
|
||||
"standardBodyId": "130",
|
||||
},
|
||||
Object {
|
||||
"id": "48",
|
||||
"name": "Tuskaninny",
|
||||
"standardBodyId": "188",
|
||||
},
|
||||
Object {
|
||||
"id": "49",
|
||||
"name": "Uni",
|
||||
"standardBodyId": "257",
|
||||
},
|
||||
Object {
|
||||
"id": "50",
|
||||
"name": "Usul",
|
||||
"standardBodyId": "206",
|
||||
},
|
||||
Object {
|
||||
"id": "51",
|
||||
"name": "Wocky",
|
||||
"standardBodyId": "101",
|
||||
},
|
||||
Object {
|
||||
"id": "52",
|
||||
"name": "Xweetok",
|
||||
"standardBodyId": "68",
|
||||
},
|
||||
Object {
|
||||
"id": "53",
|
||||
"name": "Yurble",
|
||||
"standardBodyId": "182",
|
||||
},
|
||||
Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
"standardBodyId": "180",
|
||||
},
|
||||
Object {
|
||||
"id": "55",
|
||||
"name": "Vandagyre",
|
||||
"standardBodyId": "306",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue