From bcdd9af806dfe9253d4d515a9a3fe0710f0e18a1 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 23 May 2020 13:23:24 -0700 Subject: [PATCH] transition to closest valid pose for species/color --- src/app/HomePage.js | 11 ++- src/app/OutfitControls.js | 4 +- src/app/SpeciesColorPicker.js | 136 +++++++++++++++++++++++++++++++-- src/app/useOutfitState.js | 1 + src/server/getValidPetPoses.js | 4 +- 5 files changed, 146 insertions(+), 10 deletions(-) diff --git a/src/app/HomePage.js b/src/app/HomePage.js index 0a26efb..9b3aec8 100644 --- a/src/app/HomePage.js +++ b/src/app/HomePage.js @@ -48,9 +48,15 @@ function HomePage() { function StartOutfitForm() { const history = useHistory(); + const idealPose = React.useMemo( + () => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"), + [] + ); + const [speciesId, setSpeciesId] = React.useState("1"); const [colorId, setColorId] = React.useState("8"); const [isValid, setIsValid] = React.useState(true); + const [closestPose, setClosestPose] = React.useState(idealPose); const onSubmit = (e) => { e.preventDefault(); @@ -62,6 +68,7 @@ function StartOutfitForm() { const params = new URLSearchParams({ species: speciesId, color: colorId, + pose: closestPose, }); history.push(`/outfits/new?${params}`); @@ -73,11 +80,13 @@ function StartOutfitForm() { { + onChange={(species, color, isValid, closestPose) => { setSpeciesId(species.id); setColorId(color.id); setIsValid(isValid); + setClosestPose(closestPose); }} /> diff --git a/src/app/OutfitControls.js b/src/app/OutfitControls.js index 73c9897..08a03ab 100644 --- a/src/app/OutfitControls.js +++ b/src/app/OutfitControls.js @@ -87,13 +87,15 @@ function OutfitControls({ outfitState, dispatchToOutfit }) { { + onChange={(species, color, isValid, closestPose) => { if (isValid) { dispatchToOutfit({ type: "setSpeciesAndColor", speciesId: species.id, colorId: color.id, + pose: closestPose, }); } else { toast({ diff --git a/src/app/SpeciesColorPicker.js b/src/app/SpeciesColorPicker.js index 5b14cd9..e8715c9 100644 --- a/src/app/SpeciesColorPicker.js +++ b/src/app/SpeciesColorPicker.js @@ -14,6 +14,7 @@ import { Delay, useFetch } from "./util"; function SpeciesColorPicker({ speciesId, colorId, + idealPose, showPlaceholders, dark = false, onChange, @@ -99,7 +100,11 @@ function SpeciesColorPicker({ const species = allSpecies.find((s) => s.id === speciesId); const newColor = allColors.find((c) => c.id === newColorId); - onChange(species, newColor, pairIsValid(valids, speciesId, newColorId)); + const validPoses = getValidPoses(valids, speciesId, newColorId); + const isValid = validPoses.size > 0; + const closestPose = getClosestPose(validPoses, idealPose); + console.log(idealPose, closestPose, validPoses); + onChange(species, newColor, isValid, closestPose); }; // When the species changes, check if the new pair is valid, and update the @@ -109,7 +114,11 @@ function SpeciesColorPicker({ const newSpecies = allSpecies.find((s) => s.id === newSpeciesId); const color = allColors.find((c) => c.id === colorId); - onChange(newSpecies, color, pairIsValid(valids, newSpeciesId, colorId)); + const validPoses = getValidPoses(valids, newSpeciesId, colorId); + const isValid = validPoses.size > 0; + const closestPose = getClosestPose(validPoses, idealPose); + console.log(idealPose, closestPose, validPoses); + onChange(newSpecies, color, isValid, closestPose); }; return ( @@ -157,14 +166,131 @@ function SpeciesColorPicker({ ); } -function pairIsValid(valids, speciesId, colorId) { +function getPairByte(valids, speciesId, colorId) { // Reading a bit table, owo! const speciesIndex = speciesId - 1; const colorIndex = colorId - 1; const numColors = valids.getUint8(1); const pairByteIndex = speciesIndex * numColors + colorIndex + 2; - const pairByte = valids.getUint8(pairByteIndex); - return pairByte !== 0; + return valids.getUint8(pairByteIndex); } +function pairIsValid(valids, speciesId, colorId) { + return getPairByte(valids, speciesId, colorId) !== 0; +} + +function getValidPoses(valids, speciesId, colorId) { + const pairByte = getPairByte(valids, speciesId, colorId); + console.log("pair byte", pairByte.toString(2).padStart(8, "0")); + + const validPoses = new Set(); + if (pairByte & 0b00000001) validPoses.add("HAPPY_MASC"); + if (pairByte & 0b00000010) validPoses.add("SAD_MASC"); + if (pairByte & 0b00000100) validPoses.add("SICK_MASC"); + if (pairByte & 0b00001000) validPoses.add("HAPPY_FEM"); + if (pairByte & 0b00010000) validPoses.add("SAD_FEM"); + if (pairByte & 0b00100000) validPoses.add("SICK_FEM"); + // TODO: Add unconverted support! + // if (pairByte & 0b01000000) validPoses.add("UNCONVERTED"); + if (pairByte & 0b10000000) validPoses.add("UNKNOWN"); + + return validPoses; +} + +function getClosestPose(validPoses, idealPose) { + return closestPosesInOrder[idealPose].find((p) => validPoses.has(p)) || null; +} + +// For each pose, in what order do we prefer to match other poses? +// +// The principles of this ordering are: +// - Happy/sad matters more than gender presentation. +// - "Sick" is an unpopular emotion, and it's better to change gender +// presentation and stay happy/sad than to become sick. +// - Sad is a better fallback for sick than happy. +// - Unconverted vs converted is the biggest possible difference. +// - Unknown is the pose of last resort - even coming from another unknown. +const closestPosesInOrder = { + HAPPY_MASC: [ + "HAPPY_MASC", + "HAPPY_FEM", + "SAD_MASC", + "SAD_FEM", + "SICK_MASC", + "SICK_FEM", + "UNCONVERTED", + "UNKNOWN", + ], + HAPPY_FEM: [ + "HAPPY_FEM", + "HAPPY_MASC", + "SAD_FEM", + "SAD_MASC", + "SICK_FEM", + "SICK_MASC", + "UNCONVERTED", + "UNKNOWN", + ], + SAD_MASC: [ + "SAD_MASC", + "SAD_FEM", + "HAPPY_MASC", + "HAPPY_FEM", + "SICK_MASC", + "SICK_FEM", + "UNCONVERTED", + "UNKNOWN", + ], + SAD_FEM: [ + "SAD_FEM", + "SAD_MASC", + "HAPPY_FEM", + "HAPPY_MASC", + "SICK_FEM", + "SICK_MASC", + "UNCONVERTED", + "UNKNOWN", + ], + SICK_MASC: [ + "SICK_MASC", + "SICK_FEM", + "SAD_MASC", + "SAD_FEM", + "HAPPY_MASC", + "HAPPY_FEM", + "UNCONVERTED", + "UNKNOWN", + ], + SICK_FEM: [ + "SICK_FEM", + "SICK_MASC", + "SAD_FEM", + "SAD_MASC", + "HAPPY_FEM", + "HAPPY_MASC", + "UNCONVERTED", + "UNKNOWN", + ], + UNCONVERTED: [ + "UNCONVERTED", + "HAPPY_FEM", + "HAPPY_MASC", + "SAD_FEM", + "SAD_MASC", + "SICK_FEM", + "SICK_MASC", + "UNKNOWN", + ], + UNKNOWN: [ + "HAPPY_FEM", + "HAPPY_MASC", + "SAD_FEM", + "SAD_MASC", + "SICK_FEM", + "SICK_MASC", + "UNCONVERTED", + "UNKNOWN", + ], +}; + export default SpeciesColorPicker; diff --git a/src/app/useOutfitState.js b/src/app/useOutfitState.js index 45bc358..dba0e3b 100644 --- a/src/app/useOutfitState.js +++ b/src/app/useOutfitState.js @@ -105,6 +105,7 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => { ...baseState, speciesId: action.speciesId, colorId: action.colorId, + pose: action.pose, }; case "wearItem": return produce(baseState, (state) => { diff --git a/src/server/getValidPetPoses.js b/src/server/getValidPetPoses.js index 0cd3972..bccbc45 100644 --- a/src/server/getValidPetPoses.js +++ b/src/server/getValidPetPoses.js @@ -38,9 +38,7 @@ export default async function getValidPetPoses() { for (let colorId = 1; colorId <= numColors; colorId++) { const colorIndex = colorId - 1; - // We fill in the high bits first. If we add more things later, write - // them first, so that they fill in the currently-empty high bits and - // everything else stays in the same position as before! + // We fill in the high bits first, and shift left as we go! let byte = 0; byte += hasPose(speciesId, colorId, "UNKNOWN") ? 1 : 0; byte <<= 1;