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:
parent
92db11b995
commit
17d75ee97f
2 changed files with 103 additions and 28 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue