Add color and step-value support for bulk add

This is enough to start fixing items like Baby in a Pumpkin! Hooray! 😁
This commit is contained in:
Emi Matchu 2021-03-15 13:28:16 -07:00
parent 92db11b995
commit 17d75ee97f
2 changed files with 103 additions and 28 deletions

View file

@ -68,7 +68,9 @@ function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) {
const [minAssetId, setMinAssetId] = React.useState( const [minAssetId, setMinAssetId] = React.useState(
bulkAddProposal?.minAssetId bulkAddProposal?.minAssetId
); );
const [assetIdStepValue, setAssetIdStepValue] = React.useState(1);
const [numSpecies, setNumSpecies] = React.useState(55); const [numSpecies, setNumSpecies] = React.useState(55);
const [colorId, setColorId] = React.useState("8");
return ( return (
<Flex <Flex
@ -79,7 +81,7 @@ function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) {
transition="0.2s all" transition="0.2s all"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
onSubmit({ minAssetId, numSpecies }); onSubmit({ minAssetId, numSpecies, assetIdStepValue, colorId });
}} }}
> >
<Tooltip <Tooltip
@ -98,7 +100,7 @@ function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) {
> >
<Flex align="center" tabIndex="0"> <Flex align="center" tabIndex="0">
<EditIcon marginRight="1" /> <EditIcon marginRight="1" />
<Box>Bulk-add body-specific assets:</Box> <Box>Bulk-add:</Box>
</Flex> </Flex>
</Tooltip> </Tooltip>
<Box width="2" /> <Box width="2" />
@ -124,14 +126,31 @@ function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) {
placeholder="Max ID" placeholder="Max ID"
// Because this is an inclusive range, the offset between the numbers // Because this is an inclusive range, the offset between the numbers
// is one less than the number of entries in the range. // is one less than the number of entries in the range.
value={minAssetId != null ? Number(minAssetId) + (numSpecies - 1) : ""} value={
minAssetId != null
? Number(minAssetId) + assetIdStepValue * (numSpecies - 1)
: ""
}
onChange={(e) => onChange={(e) =>
setMinAssetId( setMinAssetId(
e.target.value ? Number(e.target.value) - (numSpecies - 1) : null e.target.value
? Number(e.target.value) - assetIdStepValue * (numSpecies - 1)
: null
) )
} }
/> />
<Box width="1" /> <Box width="1" />
<Select
size="xs"
width="12ch"
value={String(assetIdStepValue)}
onChange={(e) => setAssetIdStepValue(Number(e.target.value))}
>
<option value="1">(All IDs)</option>
<option value="2">(Every other ID)</option>
<option value="3">(Every 3rd ID)</option>
</Select>
<Box width="1" />
for for
<Box width="1" /> <Box width="1" />
<Select <Select
@ -143,6 +162,17 @@ function BulkAddBodySpecificAssetsForm({ bulkAddProposal, onSubmit }) {
<option value="55">All 55 species</option> <option value="55">All 55 species</option>
<option value="54">54 species, no Vandagyre</option> <option value="54">54 species, no Vandagyre</option>
</Select> </Select>
<Box width="1" />
<Select
size="xs"
width="20ch"
value={colorId}
onChange={(e) => setColorId(e.target.value)}
>
<option value="8">All standard colors</option>
<option value="6">Baby</option>
<option value="46">Mutant</option>
</Select>
<Box width="2" /> <Box width="2" />
<Button type="submit" size="xs" isDisabled={minAssetId == null}> <Button type="submit" size="xs" isDisabled={minAssetId == null}>
Preview Preview
@ -208,7 +238,10 @@ function AllItemLayersSupportModalContent({
data: bulkAddProposalData, data: bulkAddProposalData,
} = useQuery( } = useQuery(
gql` gql`
query AllItemLayersSupportModal_BulkAddProposal($layerRemoteIds: [ID!]!) { query AllItemLayersSupportModal_BulkAddProposal(
$layerRemoteIds: [ID!]!
$colorId: ID!
) {
layersToAdd: itemAppearanceLayersByRemoteId( layersToAdd: itemAppearanceLayersByRemoteId(
remoteIds: $layerRemoteIds remoteIds: $layerRemoteIds
) { ) {
@ -216,23 +249,33 @@ function AllItemLayersSupportModalContent({
...AppearanceLayerForOutfitPreview ...AppearanceLayerForOutfitPreview
} }
allSpecies { color(id: $colorId) {
id id
name appliedToAllCompatibleSpecies {
standardBodyId
canonicalAppearance {
id id
species { species {
id id
name name
} }
color { body {
id id
name
isStandard
} }
pose canonicalAppearance {
...PetAppearanceForOutfitPreview # 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
}
} }
} }
} }
@ -244,9 +287,13 @@ function AllItemLayersSupportModalContent({
variables: { variables: {
layerRemoteIds: bulkAddProposal layerRemoteIds: bulkAddProposal
? Array.from({ length: 54 }, (_, i) => ? Array.from({ length: 54 }, (_, i) =>
String(Number(bulkAddProposal.minAssetId) + i) String(
Number(bulkAddProposal.minAssetId) +
i * bulkAddProposal.assetIdStepValue
)
) )
: [], : [],
colorId: bulkAddProposal?.colorId,
}, },
skip: bulkAddProposal == null, skip: bulkAddProposal == null,
} }
@ -319,6 +366,12 @@ function AllItemLayersSupportModalContent({
colorScheme="green" colorScheme="green"
isLoading={mutationLoading} isLoading={mutationLoading}
onClick={() => { onClick={() => {
if (
!window.confirm("Are you sure? Bulk operations are dangerous!")
) {
return;
}
// HACK: This could pick up not just new layers, but existing layers // HACK: This could pick up not just new layers, but existing layers
// that aren't changing. Shouldn't be a problem to save, // that aren't changing. Shouldn't be a problem to save,
// though? // though?
@ -443,7 +496,7 @@ function mergeBulkAddProposalIntoItemAppearances(
return itemAppearances; return itemAppearances;
} }
const { allSpecies, layersToAdd } = bulkAddProposalData; const { color, layersToAdd } = bulkAddProposalData;
// Do a deep copy of the existing item appearances, so we can mutate them as // Do a deep copy of the existing item appearances, so we can mutate them as
// we loop through them in this function! // we loop through them in this function!
@ -451,41 +504,42 @@ function mergeBulkAddProposalIntoItemAppearances(
// To exclude Vandagyre, we take the first N species by ID - which is // To exclude Vandagyre, we take the first N species by ID - which is
// different than the alphabetical sort order we use for assigning layers! // different than the alphabetical sort order we use for assigning layers!
const speciesToInclude = [...allSpecies] const speciesColorPairsToInclude = [...color.appliedToAllCompatibleSpecies]
.sort((a, b) => Number(a.id) - Number(b.id)) .sort((a, b) => Number(a.species.id) - Number(b.species.id))
.slice(0, bulkAddProposal.numSpecies); .slice(0, bulkAddProposal.numSpecies);
// Set up the incoming data in convenient formats. // Set up the incoming data in convenient formats.
const sortedSpecies = [...speciesToInclude].sort((a, b) => const sortedSpeciesColorPairs = [...speciesColorPairsToInclude].sort((a, b) =>
a.name.localeCompare(b.name) a.species.name.localeCompare(b.species.name)
); );
const layersToAddByRemoteId = {}; const layersToAddByRemoteId = {};
for (const layer of layersToAdd) { for (const layer of layersToAdd) {
layersToAddByRemoteId[layer.remoteId] = layer; layersToAddByRemoteId[layer.remoteId] = layer;
} }
for (const [index, species] of sortedSpecies.entries()) { 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 // Find the existing item appearance to add to, or create a new one if it
// doesn't exist yet. // doesn't exist yet.
let itemAppearance = mergedItemAppearances.find( let itemAppearance = mergedItemAppearances.find(
(a) => (a) => a.body.id === body.id && !a.body.representsAllBodies
a.body.canonicalAppearance.species.id === species.id &&
!a.body.representsAllBodies
); );
if (!itemAppearance) { if (!itemAppearance) {
itemAppearance = { itemAppearance = {
id: `bulk-add-proposal-new-item-appearance-for-body-${species.standardBodyId}`, id: `bulk-add-proposal-new-item-appearance-for-body-${body.id}`,
layers: [], layers: [],
body: { body: {
id: species.standardBodyId, id: body.id,
canonicalAppearance: species.canonicalAppearance, canonicalAppearance,
}, },
}; };
mergedItemAppearances.push(itemAppearance); mergedItemAppearances.push(itemAppearance);
} }
const layerToAddRemoteId = String( const layerToAddRemoteId = String(
Number(bulkAddProposal.minAssetId) + index Number(bulkAddProposal.minAssetId) +
index * bulkAddProposal.assetIdStepValue
); );
const layerToAdd = layersToAddByRemoteId[layerToAddRemoteId]; const layerToAdd = layersToAddByRemoteId[layerToAddRemoteId];
if (!layerToAdd) { if (!layerToAdd) {

View file

@ -89,6 +89,9 @@ const typeDefs = gql`
color: Color! color: Color!
body: Body! body: Body!
# A PetAppearance that has this species and color. Prefers happy poses.
canonicalAppearance: PetAppearance
# A hash to use in a pets.neopets.com image URL. Might be null if we don't # A hash to use in a pets.neopets.com image URL. Might be null if we don't
# have one for this pair, which is uncommon - but it's _somewhat_ common # have one for this pair, which is uncommon - but it's _somewhat_ common
# for them to have clothes, if we've never seen a plain version modeled. # for them to have clothes, if we've never seen a plain version modeled.
@ -284,6 +287,24 @@ const resolvers = {
const petType = await petTypeLoader.load(id); const petType = await petTypeLoader.load(id);
return { id: petType.bodyId }; return { id: petType.bodyId };
}, },
canonicalAppearance: async (
{ id },
_,
{ petTypeLoader, canonicalPetStateForBodyLoader }
) => {
const petType = await petTypeLoader.load(id);
const petState = await canonicalPetStateForBodyLoader.load({
bodyId: petType.bodyId,
preferredColorId: petType.colorId,
fallbackColorId: petType.colorId,
});
if (!petState) {
return null;
}
return { id: petState.id };
},
neopetsImageHash: async ({ id }, _, { petTypeLoader }) => { neopetsImageHash: async ({ id }, _, { petTypeLoader }) => {
const petType = await petTypeLoader.load(id); const petType = await petTypeLoader.load(id);