Support can label pet poses!
it's good shit, y'all
This commit is contained in:
parent
45ab35216f
commit
1ef05adce4
5 changed files with 316 additions and 83 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue