Support can label pet poses!

it's good shit, y'all
This commit is contained in:
Emi Matchu 2020-08-31 00:32:17 -07:00
parent 45ab35216f
commit 1ef05adce4
5 changed files with 316 additions and 83 deletions

View file

@ -14,8 +14,9 @@ GRANT SELECT ON openneo_impress.zone_translations TO impress2020;
-- Public data tables: write
GRANT UPDATE ON openneo_impress.items TO impress2020;
GRANT UPDATE ON openneo_impress.swf_assets TO impress2020;
GRANT DELETE ON openneo_impress.parents_swf_assets TO impress2020;
GRANT UPDATE ON openneo_impress.pet_states TO impress2020;
GRANT UPDATE ON openneo_impress.swf_assets TO impress2020;
-- User data tables
GRANT SELECT ON openneo_impress.item_outfit_relationships TO impress2020;

View file

@ -1,11 +1,16 @@
import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { Box, IconButton, Select, Switch } from "@chakra-ui/core";
import { ArrowBackIcon, ArrowForwardIcon } from "@chakra-ui/icons";
import { useMutation, useQuery } from "@apollo/client";
import { Box, IconButton, Select, Spinner, Switch } from "@chakra-ui/core";
import {
ArrowBackIcon,
ArrowForwardIcon,
CheckCircleIcon,
} from "@chakra-ui/icons";
import HangerSpinner from "../../components/HangerSpinner";
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
import useSupport from "./useSupport";
function PosePickerSupport({
speciesId,
@ -30,56 +35,9 @@ function PosePickerSupport({
}
}
happyMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_MASC
) {
id
}
sadMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_MASC
) {
id
}
sickMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_MASC
) {
id
}
happyFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_FEM
) {
id
}
sadFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_FEM
) {
id
}
sickFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_FEM
) {
id
}
unknown: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: UNKNOWN
) {
id
}
...CanonicalPetAppearances
}
${canonicalPetAppearancesFragment}
`,
{ variables: { speciesId, colorId } }
);
@ -121,6 +79,7 @@ function PosePickerSupport({
HAPPY_FEM: data.happyFem?.id,
SAD_FEM: data.sadFem?.id,
SICK_FEM: data.sickFem?.id,
UNCONVERTED: data.unconverted?.id,
UNKNOWN: data.unknown?.id,
};
const canonicalAppearanceIds = Object.values(
@ -150,37 +109,21 @@ function PosePickerSupport({
canonicalAppearanceIds={canonicalAppearanceIds}
dispatchToOutfit={dispatchToOutfit}
/>
<Metadata fontSize="sm">
<Metadata
fontSize="sm"
// Build a new copy of this tree when the appearance changes, to reset
// things like element focus and mutation state!
key={currentPetAppearance.id}
>
<MetadataLabel>DTI ID:</MetadataLabel>
<MetadataValue>{appearanceId}</MetadataValue>
<MetadataLabel>Pose:</MetadataLabel>
<MetadataValue>
<Box display="flex" flexDirection="row" alignItems="center">
<Select
size="sm"
value={currentPetAppearance.pose}
flex="0 1 200px"
cursor="not-allowed"
isReadOnly
>
{Object.entries(POSE_NAMES).map(([pose, name]) => (
<option key={pose} value={pose}>
{name}
</option>
))}
</Select>
<Select
size="sm"
marginLeft="2"
flex="0 1 150px"
value={currentPetAppearance.isGlitched}
cursor="not-allowed"
isReadOnly
>
<option value="false">Usable</option>
<option value="true">Glitched</option>
</Select>
</Box>
<PosePickerSupportPoseFields
petAppearance={currentPetAppearance}
speciesId={speciesId}
colorId={colorId}
/>
</MetadataValue>
<MetadataLabel>Zones:</MetadataLabel>
<MetadataValue>
@ -267,6 +210,97 @@ function PosePickerSupportNavigator({
);
}
function PosePickerSupportPoseFields({ petAppearance, speciesId, colorId }) {
const { supportSecret } = useSupport();
const [mutate, { loading, error, data }] = useMutation(
gql`
mutation PosePickerSupportSetPetAppearancePose(
$appearanceId: ID!
$pose: Pose!
$supportSecret: String!
) {
setPetAppearancePose(
appearanceId: $appearanceId
pose: $pose
supportSecret: $supportSecret
) {
id
pose
}
}
`,
{
refetchQueries: [
{
query: gql`
query PosePickerSupportRefetchCanonicalAppearances(
$speciesId: ID!
$colorId: ID!
) {
...CanonicalPetAppearances
}
${canonicalPetAppearancesFragment}
`,
variables: { speciesId, colorId },
},
],
}
);
return (
<Box>
<Box display="flex" flexDirection="row" alignItems="center">
<Select
size="sm"
value={petAppearance.pose}
flex="0 1 200px"
icon={loading ? <Spinner /> : data ? <CheckCircleIcon /> : undefined}
onChange={(e) => {
const pose = e.target.value;
mutate({
variables: {
appearanceId: petAppearance.id,
pose,
supportSecret,
},
optimisticResponse: {
__typename: "Mutation",
setPetAppearancePose: {
__typename: "PetAppearance",
id: petAppearance.id,
pose,
},
},
}).catch((e) => {
/* Discard errors here; we'll show them in the UI! */
});
}}
isInvalid={error != null}
>
{Object.entries(POSE_NAMES).map(([pose, name]) => (
<option key={pose} value={pose}>
{name}
</option>
))}
</Select>
<Select
size="sm"
marginLeft="2"
flex="0 1 150px"
value={petAppearance.isGlitched}
cursor="not-allowed"
isReadOnly
>
<option value="false">Valid</option>
<option value="true">Glitched</option>
</Select>
</Box>
{error && <Box color="red.400">{error.message}</Box>}
</Box>
);
}
export function PosePickerSupportSwitch({ isChecked, onChange }) {
return (
<Box as="label" display="flex" flexDirection="row" alignItems="center">
@ -297,4 +331,72 @@ const POSE_NAMES = {
UNKNOWN: "Unknown",
};
const canonicalPetAppearancesFragment = gql`
fragment CanonicalPetAppearances on Query {
happyMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_MASC
) {
id
}
sadMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_MASC
) {
id
}
sickMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_MASC
) {
id
}
happyFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_FEM
) {
id
}
sadFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_FEM
) {
id
}
sickFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_FEM
) {
id
}
unconverted: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: UNCONVERTED
) {
id
}
unknown: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: UNKNOWN
) {
id
}
}
`;
export default PosePickerSupport;

View file

@ -7,9 +7,11 @@ const neopets = require("./neopets");
const {
capitalize,
getPoseFromPetState,
getPetStateFieldsFromPose,
getPoseFromPetData,
getEmotion,
getGenderPresentation,
getPoseName,
loadBodyName,
logToDiscord,
normalizeRow,
@ -259,6 +261,12 @@ const typeDefs = gql`
itemId: ID!
supportSecret: String!
): RemoveLayerFromItemMutationResult!
setPetAppearancePose(
appearanceId: ID!
pose: Pose!
supportSecret: String!
): PetAppearance!
}
`;
@ -939,6 +947,88 @@ const resolvers = {
return { layer: { id: layerId }, item: { id: itemId } };
},
setPetAppearancePose: async (
_,
{ appearanceId, pose, supportSecret },
{
colorTranslationLoader,
speciesTranslationLoader,
petStateLoader,
petTypeLoader,
db,
}
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldPetState = await petStateLoader.load(appearanceId);
const { moodId, female, unconverted } = getPetStateFieldsFromPose(pose);
const [result] = await db.execute(
`UPDATE pet_states SET mood_id = ?, female = ?, unconverted = ?
WHERE id = ? LIMIT 1`,
[moodId, female, unconverted, appearanceId]
);
if (result.affectedRows !== 1) {
throw new Error(
`Expected to affect 1 layer, but affected ${result.affectedRows}`
);
}
// we changed it, so clear it from cache
petStateLoader.clear(appearanceId);
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
try {
const petType = await petTypeLoader.load(oldPetState.petTypeId);
const [colorTranslation, speciesTranslation] = await Promise.all([
colorTranslationLoader.load(petType.colorId),
speciesTranslationLoader.load(petType.speciesId),
]);
const oldPose = getPoseFromPetState(oldPetState);
const colorName = capitalize(colorTranslation.name);
const speciesName = capitalize(speciesTranslation.name);
await logToDiscord({
embeds: [
{
title: `🛠 ${colorName} ${speciesName}`,
thumbnail: {
url: `http://pets.neopets.com/cp/${
petType.basicImageHash || petType.imageHash
}/1/6.png`,
height: 150,
width: 150,
},
fields: [
{
name: `Appearance ${appearanceId}: Pose`,
value: `${getPoseName(oldPose)} → **${getPoseName(pose)}**`,
},
{
name: "As a reminder…",
value: "…the thumbnail might not match!",
},
],
timestamp: new Date().toISOString(),
url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${appearanceId}`,
},
],
});
} catch (e) {
console.error("Error sending Discord support log", e);
}
} else {
console.warn("No Discord support webhook provided, skipping");
}
return { id: appearanceId };
},
},
};

View file

@ -355,7 +355,8 @@ const buildPetStatesForPetTypeLoader = (db, loaders) =>
const [rows, _] = await db.execute(
`SELECT * FROM pet_states
WHERE pet_type_id IN (${qs})
ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC, id DESC`,
ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC,
unconverted DESC, id DESC`,
petTypeIds
);

View file

@ -21,9 +21,9 @@ function getEmotion(pose) {
function getGenderPresentation(pose) {
if (["HAPPY_MASC", "SAD_MASC", "SICK_MASC"].includes(pose)) {
return "MASCULINE";
return "MASC";
} else if (["HAPPY_FEM", "SAD_FEM", "SICK_FEM"].includes(pose)) {
return "MASCULINE";
return "FEM";
} else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
return null;
} else {
@ -60,6 +60,28 @@ function getPoseFromPetState(petState) {
}
}
function getPetStateFieldsFromPose(pose) {
if (pose === "UNCONVERTED") {
return { moodId: null, female: null, unconverted: true };
} else if (pose === "UNKNOWN") {
return { moodId: null, female: null, unconverted: false };
} else if (pose === "HAPPY_MASC") {
return { moodId: "1", female: false, unconverted: false };
} else if (pose === "HAPPY_FEM") {
return { moodId: "1", female: true, unconverted: false };
} else if (pose === "SAD_MASC") {
return { moodId: "2", female: false, unconverted: false };
} else if (pose === "SAD_FEM") {
return { moodId: "2", female: true, unconverted: false };
} else if (pose === "SICK_MASC") {
return { moodId: "3", female: false, unconverted: false };
} else if (pose === "SICK_FEM") {
return { moodId: "3", female: true, unconverted: false };
} else {
throw new Error(`unexpected pose ${pose}`);
}
}
function getPoseFromPetData(petMetaData, petCustomData) {
// TODO: Use custom data to decide if Unconverted.
const moodId = petMetaData.mood;
@ -85,6 +107,21 @@ function getPoseFromPetData(petMetaData, petCustomData) {
}
}
const POSE_NAMES = {
HAPPY_MASC: "Happy Masc",
SAD_MASC: "Sad Masc",
SICK_MASC: "Sick Masc",
HAPPY_FEM: "Happy Fem",
SAD_FEM: "Sad Fem",
SICK_FEM: "Sick Fem",
UNCONVERTED: "Unconverted",
UNKNOWN: "Unknown",
};
function getPoseName(pose) {
return POSE_NAMES[pose];
}
async function loadBodyName(bodyId, db) {
if (String(bodyId) === "0") {
return "All bodies";
@ -148,7 +185,9 @@ module.exports = {
getEmotion,
getGenderPresentation,
getPoseFromPetState,
getPetStateFieldsFromPose,
getPoseFromPetData,
getPoseName,
loadBodyName,
logToDiscord,
normalizeRow,