diff --git a/api/validPetPoses.js b/api/validPetPoses.js new file mode 100644 index 0000000..027caea --- /dev/null +++ b/api/validPetPoses.js @@ -0,0 +1,6 @@ +import getValidPetPoses from "../src/server/getValidPetPoses"; + +export default async (req, res) => { + const buffer = await getValidPetPoses(); + res.status(200).send(buffer); +}; diff --git a/package.json b/package.json index 2168b32..6d519c6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "react-dom": "^16.13.1", "react-helmet": "^6.0.0", "react-scripts": "3.4.1", - "react-transition-group": "^4.3.0" + "react-transition-group": "^4.3.0", + "use-http": "^1.0.10" }, "scripts": { "start": "react-scripts start", diff --git a/src/app/SpeciesColorPicker.js b/src/app/SpeciesColorPicker.js index 79f41de..8844533 100644 --- a/src/app/SpeciesColorPicker.js +++ b/src/app/SpeciesColorPicker.js @@ -1,5 +1,6 @@ import React from "react"; import gql from "graphql-tag"; +import useFetch from "use-http"; import { useQuery } from "@apollo/react-hooks"; import { Box, Flex, Select, Text, useToast } from "@chakra-ui/core"; @@ -13,7 +14,7 @@ import { Delay } from "./util"; */ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { const toast = useToast(); - const { loading, error, data } = useQuery(gql` + const { loading: loadingMeta, error: errorMeta, data: meta } = useQuery(gql` query { allSpecies { id @@ -24,35 +25,24 @@ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { id name } - - allValidSpeciesColorPairs { - species { - id - } - color { - id - } - } } `); - - const allColors = (data && [...data.allColors]) || []; - allColors.sort((a, b) => a.name.localeCompare(b.name)); - const allSpecies = (data && [...data.allSpecies]) || []; - allSpecies.sort((a, b) => a.name.localeCompare(b.name)); - - // Build a large Set where we can quickly look up species/color pairs! - const allValidSpeciesColorPairs = React.useMemo( - () => - new Set( - ((data && data.allValidSpeciesColorPairs) || []).map( - (p) => `${p.species.id},${p.color.id}` - ) - ), - [data] + const { + loading: loadingValids, + error: errorValids, + data: validsBuffer, + } = useFetch("/api/validPetPoses", { responseType: "arrayBuffer" }, []); + const valids = React.useMemo( + () => validsBuffer && new DataView(validsBuffer), + [validsBuffer] ); - if (loading) { + const allColors = (meta && [...meta.allColors]) || []; + allColors.sort((a, b) => a.name.localeCompare(b.name)); + const allSpecies = (meta && [...meta.allSpecies]) || []; + allSpecies.sort((a, b) => a.name.localeCompare(b.name)); + + if (loadingMeta || loadingValids) { return ( @@ -62,7 +52,7 @@ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { ); } - if (error) { + if (errorMeta || errorValids) { return ( Error loading species/color data. @@ -75,8 +65,7 @@ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { const onChangeColor = (e) => { const speciesId = outfitState.speciesId; const colorId = e.target.value; - const pair = `${speciesId},${colorId}`; - if (allValidSpeciesColorPairs.has(pair)) { + if (pairIsValid(valids, meta, speciesId, colorId)) { dispatchToOutfit({ type: "changeColor", colorId: e.target.value }); } else { const species = allSpecies.find((s) => s.id === speciesId); @@ -93,14 +82,14 @@ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { const onChangeSpecies = (e) => { const colorId = outfitState.colorId; const speciesId = e.target.value; - const pair = `${speciesId},${colorId}`; - if (allValidSpeciesColorPairs.has(pair)) { + if (pairIsValid(valids, meta, speciesId, colorId)) { dispatchToOutfit({ type: "changeSpecies", speciesId: e.target.value }); } else { const species = allSpecies.find((s) => s.id === speciesId); const color = allColors.find((c) => c.id === colorId); toast({ title: `We haven't seen a ${color.name} ${species.name} before! 😓`, + status: "warning", }); } }; @@ -144,4 +133,14 @@ function SpeciesColorPicker({ outfitState, dispatchToOutfit }) { ); } +function pairIsValid(valids, meta, speciesId, colorId) { + // Reading a bit table, owo! + const speciesIndex = speciesId - 1; + const colorIndex = colorId - 1; + const numColors = meta.allColors.length; + const pairByteIndex = speciesIndex * numColors + colorIndex; + const pairByte = valids.getUint8(pairByteIndex); + return pairByte !== 0; +} + export default SpeciesColorPicker; diff --git a/src/server/__snapshots__/getValidPetPoses.test.js.snap b/src/server/__snapshots__/getValidPetPoses.test.js.snap new file mode 100644 index 0000000..4028d80 Binary files /dev/null and b/src/server/__snapshots__/getValidPetPoses.test.js.snap differ diff --git a/src/server/getValidPetPoses.js b/src/server/getValidPetPoses.js new file mode 100644 index 0000000..79ab539 --- /dev/null +++ b/src/server/getValidPetPoses.js @@ -0,0 +1,78 @@ +import connectToDb from "./db"; + +import { getEmotion, getGenderPresentation } from "./util"; + +export default async function getValidPetPoses() { + const db = await connectToDb(); + + const numSpeciesPromise = getNumSpecies(db); + const numColorsPromise = getNumColors(db); + const poseTuplesPromise = getPoseTuples(db); + + const [numSpecies, numColors, poseTuples] = await Promise.all([ + numSpeciesPromise, + numColorsPromise, + poseTuplesPromise, + ]); + + const poseStrs = new Set(); + for (const poseTuple of poseTuples) { + const { species_id, color_id, mood_id, female } = poseTuple; + const emotion = getEmotion(mood_id); + const genderPresentation = getGenderPresentation(female); + const poseStr = `${species_id}-${color_id}-${emotion}-${genderPresentation}`; + poseStrs.add(poseStr); + } + + function hasPose(speciesId, colorId, emotion, genderPresentation) { + const poseStr = `${speciesId}-${colorId}-${emotion}-${genderPresentation}`; + return poseStrs.has(poseStr); + } + + const numPairs = numSpecies * numColors; + const buffer = Buffer.alloc(numPairs); + + for (let speciesId = 1; speciesId <= numSpecies; speciesId++) { + const speciesIndex = speciesId - 1; + for (let colorId = 1; colorId <= numColors; colorId++) { + const colorIndex = colorId - 1; + + let byte = 0; + byte += hasPose(speciesId, colorId, "HAPPY", "MASCULINE") ? 1 : 0; + byte <<= 1; + byte += hasPose(speciesId, colorId, "SAD", "MASCULINE") ? 1 : 0; + byte <<= 1; + byte += hasPose(speciesId, colorId, "SICK", "MASCULINE") ? 1 : 0; + byte <<= 1; + byte += hasPose(speciesId, colorId, "HAPPY", "FEMININE") ? 1 : 0; + byte <<= 1; + byte += hasPose(speciesId, colorId, "SAD", "FEMININE") ? 1 : 0; + byte <<= 1; + byte += hasPose(speciesId, colorId, "SICK", "FEMININE") ? 1 : 0; + + buffer.writeUInt8(byte, speciesIndex * numColors + colorIndex); + } + } + + return buffer; +} + +async function getNumSpecies(db) { + const [rows, _] = await db.query(`SELECT count(*) FROM species`); + return rows[0]["count(*)"]; +} + +async function getNumColors(db) { + const [rows, _] = await db.query( + `SELECT count(*) FROM colors WHERE prank = 0` + ); + return rows[0]["count(*)"]; +} + +async function getPoseTuples(db) { + const [rows, _] = await db.query(` + SELECT DISTINCT species_id, color_id, mood_id, female FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE mood_id IS NOT NULL AND female IS NOT NULL AND color_id >= 1`); + return rows; +} diff --git a/src/server/getValidPetPoses.test.js b/src/server/getValidPetPoses.test.js new file mode 100644 index 0000000..18ee109 --- /dev/null +++ b/src/server/getValidPetPoses.test.js @@ -0,0 +1,8 @@ +import getValidPetPoses from "./getValidPetPoses"; + +describe("getValidPetPoses", () => { + it("gets them and writes them to a buffer", async () => { + const buffer = await getValidPetPoses(); + expect(buffer.toString()).toMatchSnapshot(); + }); +}); diff --git a/src/server/index.js b/src/server/index.js index f56f077..4bb451e 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -101,8 +101,7 @@ const typeDefs = gql` type Query { allColors: [Color!]! allSpecies: [Species!]! - allValidSpeciesColorPairs: [SpeciesColorPair!]! - + allValidSpeciesColorPairs: [SpeciesColorPair!]! # deprecated items(ids: [ID!]!): [Item!]! itemSearch(query: String!): ItemSearchResult! itemSearchToFit( diff --git a/src/server/util.js b/src/server/util.js index ef692ea..49b7f15 100644 --- a/src/server/util.js +++ b/src/server/util.js @@ -3,11 +3,11 @@ function capitalize(str) { } function getEmotion(moodId) { - if (moodId === "1") { + if (String(moodId) === "1") { return "HAPPY"; - } else if (moodId === "2") { + } else if (String(moodId) === "2") { return "SAD"; - } else if (moodId === "4") { + } else if (String(moodId) === "4") { return "SICK"; } else if (moodId === null) { return null; @@ -17,9 +17,9 @@ function getEmotion(moodId) { } function getGenderPresentation(modelPetWasFemale) { - if (modelPetWasFemale === 1) { + if (String(modelPetWasFemale) === "1") { return "FEMININE"; - } else if (modelPetWasFemale === 0) { + } else if (String(modelPetWasFemale) === "0") { return "MASCULINE"; } else { return null; diff --git a/yarn.lock b/yarn.lock index 825d3c1..76d0dd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11427,6 +11427,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urs@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/urs/-/urs-0.0.4.tgz#d559d660f2a468e0bb116e0b7b505af57cb59ae4" + integrity sha512-+QflFOKa9DmjWclPB2audGCV83uWUnTXHOxLPQyu7XXcaY9yQ4+Tb3UEm8m4N7abJ0kJUCUAQBpFlq6mx80j9g== + use-callback-ref@^1.2.1: version "1.2.3" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.3.tgz#9f939dfb5740807bbf9dd79cdd4e99d27e827756" @@ -11440,6 +11445,15 @@ use-dark-mode@2.3.1: "@use-it/event-listener" "^0.1.2" use-persisted-state "^0.3.0" +use-http@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/use-http/-/use-http-1.0.10.tgz#d04da86c65552237ee13ede6218a79693b7fb452" + integrity sha512-KSxibM4WoSxnp4B366zVPOEWFadBO84yYtNEMEevupW/6V+D/Gme1Agx0cWwN6AYarrupcUoGT8P8M97jo+pzg== + dependencies: + urs "^0.0.4" + use-ssr "^1.0.22" + utility-types "^3.10.0" + use-persisted-state@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/use-persisted-state/-/use-persisted-state-0.3.0.tgz#f8e3d2fd8eee67e0c86fd596c3ea3e8121c07402" @@ -11455,6 +11469,11 @@ use-sidecar@^1.0.1: detect-node "^2.0.4" tslib "^1.9.3" +use-ssr@^1.0.22: + version "1.0.23" + resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.23.tgz#3bde1e10cd01b3b61ab6386d7cddb72e74828bf8" + integrity sha512-5bvlssgROgPgIrnILJe2mJch4e2Id0/bVm1SQzqvPvEAXmlsinCCVHWK3a2iHcPat7PkdJHBo0gmSmODIz6tNA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -11502,6 +11521,11 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"