import React from "react"; import { Box, Button, Flex, Heading, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Select, Tooltip, Wrap, WrapItem, } from "@chakra-ui/react"; import { gql, useMutation, useQuery } from "@apollo/client"; import { appearanceLayerFragment, appearanceLayerFragmentForSupport, itemAppearanceFragment, petAppearanceFragment, } from "../../components/useOutfitAppearance"; import HangerSpinner from "../../components/HangerSpinner"; import { ErrorMessage, useCommonStyles } from "../../util"; import ItemSupportAppearanceLayer from "./ItemSupportAppearanceLayer"; import { EditIcon } from "@chakra-ui/icons"; import useSupport from "./useSupport"; function AllItemLayersSupportModal({ item, isOpen, onClose }) { const [bulkAddProposal, setBulkAddProposal] = React.useState(null); const { bodyBackground } = useCommonStyles(); return ( Layers on all pets: {" "} {item.name} setBulkAddProposal(null)} /> ); } function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) { const [minAssetId, setMinAssetId] = React.useState( bulkAddProposal?.minAssetId, ); const [assetIdStepValue, setAssetIdStepValue] = React.useState(1); const [numSpecies, setNumSpecies] = React.useState(55); const [colorId, setColorId] = React.useState("8"); return ( { e.preventDefault(); onSubmit({ minAssetId, numSpecies, assetIdStepValue, colorId }); }} > When an item accidentally gets assigned to fit all bodies, this tool can help you recover the original appearances, by assuming the layer IDs are assigned to each species in alphabetical order. This will only find layers that have already been modeled! } > Bulk-add: setMinAssetId(e.target.value || null)} /> setMinAssetId( e.target.value ? Number(e.target.value) - assetIdStepValue * (numSpecies - 1) : null, ) } /> for ); } const allAppearancesFragment = gql` fragment AllAppearancesForItem on Item { allAppearances { id body { id representsAllBodies canonicalAppearance { id species { id name } color { id name isStandard } pose ...PetAppearanceForOutfitPreview } } ...ItemAppearanceForOutfitPreview } } ${itemAppearanceFragment} ${petAppearanceFragment} `; function AllItemLayersSupportModalContent({ item, bulkAddProposal, onBulkAddComplete, }) { const { supportSecret } = useSupport(); const { loading, error, data } = useQuery( gql` query AllItemLayersSupportModal($itemId: ID!) { item(id: $itemId) { id ...AllAppearancesForItem } } ${allAppearancesFragment} `, { variables: { itemId: item.id } }, ); const { loading: loading2, error: error2, data: bulkAddProposalData, } = useQuery( gql` query AllItemLayersSupportModal_BulkAddProposal( $layerRemoteIds: [ID!]! $colorId: ID! ) { layersToAdd: itemAppearanceLayersByRemoteId( remoteIds: $layerRemoteIds ) { id remoteId ...AppearanceLayerForOutfitPreview ...AppearanceLayerForSupport } color(id: $colorId) { id appliedToAllCompatibleSpecies { id species { id name } body { id } canonicalAppearance { # These are a bit redundant, but it's convenient to just reuse # what the other query is already doing. id species { id name } color { id name isStandard } pose ...PetAppearanceForOutfitPreview } } } } ${appearanceLayerFragment} ${appearanceLayerFragmentForSupport} ${petAppearanceFragment} `, { variables: { layerRemoteIds: bulkAddProposal ? Array.from({ length: 54 }, (_, i) => String( Number(bulkAddProposal.minAssetId) + i * bulkAddProposal.assetIdStepValue, ), ) : [], colorId: bulkAddProposal?.colorId, }, skip: bulkAddProposal == null, }, ); const [ sendBulkAddMutation, { loading: mutationLoading, error: mutationError }, ] = useMutation(gql` mutation AllItemLayersSupportModal_BulkAddMutation( $itemId: ID! $entries: [BulkAddLayersToItemEntry!]! $supportSecret: String! ) { bulkAddLayersToItem( itemId: $itemId entries: $entries supportSecret: $supportSecret ) { id ...AllAppearancesForItem } } ${allAppearancesFragment} `); if (loading || loading2) { return ( ); } if (error || error2) { return {(error || error2).message}; } let itemAppearances = data.item?.allAppearances || []; itemAppearances = mergeBulkAddProposalIntoItemAppearances( itemAppearances, bulkAddProposal, bulkAddProposalData, ); itemAppearances = [...itemAppearances].sort((a, b) => { const aKey = getSortKeyForBody(a.body); const bKey = getSortKeyForBody(b.body); return aKey.localeCompare(bKey); }); return ( {bulkAddProposalData && ( Previewing bulk-add changes {mutationError && ( {mutationError.message} )} )} {itemAppearances.map((itemAppearance) => ( ))} ); } function ItemAppearanceCard({ item, itemAppearance }) { const petAppearance = itemAppearance.body.canonicalAppearance; const biologyLayers = petAppearance.layers; const itemLayers = [...itemAppearance.layers].sort( (a, b) => a.zone.depth - b.zone.depth, ); const { brightBackground } = useCommonStyles(); return ( {getBodyName(itemAppearance.body)} {itemLayers.length === 0 && ( (No data) )} {itemLayers.map((itemLayer) => ( ))} ); } function getSortKeyForBody(body) { // "All bodies" sorts first! if (body.representsAllBodies) { return ""; } const { color, species } = body.canonicalAppearance; // Sort standard colors first, then special colors by name, then by species // within each color. return `${color.isStandard ? "A" : "Z"}-${color.name}-${species.name}`; } function getBodyName(body) { if (body.representsAllBodies) { return "All bodies"; } const { species, color } = body.canonicalAppearance; const speciesName = capitalize(species.name); const colorName = color.isStandard ? "Standard" : capitalize(color.name); return `${colorName} ${speciesName}`; } function capitalize(str) { return str[0].toUpperCase() + str.slice(1); } function mergeBulkAddProposalIntoItemAppearances( itemAppearances, bulkAddProposal, bulkAddProposalData, ) { if (!bulkAddProposalData) { return itemAppearances; } const { color, layersToAdd } = bulkAddProposalData; // Do a deep copy of the existing item appearances, so we can mutate them as // we loop through them in this function! const mergedItemAppearances = JSON.parse(JSON.stringify(itemAppearances)); // To exclude Vandagyre, we take the first N species by ID - which is // different than the alphabetical sort order we use for assigning layers! const speciesColorPairsToInclude = [...color.appliedToAllCompatibleSpecies] .sort((a, b) => Number(a.species.id) - Number(b.species.id)) .slice(0, bulkAddProposal.numSpecies); // Set up the incoming data in convenient formats. const sortedSpeciesColorPairs = [...speciesColorPairsToInclude].sort((a, b) => a.species.name.localeCompare(b.species.name), ); const layersToAddByRemoteId = {}; for (const layer of layersToAdd) { layersToAddByRemoteId[layer.remoteId] = layer; } for (const [index, speciesColorPair] of sortedSpeciesColorPairs.entries()) { const { body, canonicalAppearance } = speciesColorPair; // Find the existing item appearance to add to, or create a new one if it // doesn't exist yet. let itemAppearance = mergedItemAppearances.find( (a) => a.body.id === body.id && !a.body.representsAllBodies, ); if (!itemAppearance) { itemAppearance = { id: `bulk-add-proposal-new-item-appearance-for-body-${body.id}`, layers: [], body: { id: body.id, canonicalAppearance, }, }; mergedItemAppearances.push(itemAppearance); } const layerToAddRemoteId = String( Number(bulkAddProposal.minAssetId) + index * bulkAddProposal.assetIdStepValue, ); const layerToAdd = layersToAddByRemoteId[layerToAddRemoteId]; if (!layerToAdd) { continue; } // Delete this layer from other appearances (because we're going to // override its body ID), then add it to this new one. for (const otherItemAppearance of mergedItemAppearances) { const indexToDelete = otherItemAppearance.layers.findIndex( (l) => l.remoteId === layerToAddRemoteId, ); if (indexToDelete >= 0) { otherItemAppearance.layers.splice(indexToDelete, 1); } } itemAppearance.layers.push(layerToAdd); } return mergedItemAppearances; } export default AllItemLayersSupportModal;