diff --git a/cypress/integration/ItemPage/ItemZonesInfo.spec.js b/cypress/integration/ItemPage/ItemZonesInfo.spec.js
new file mode 100644
index 0000000..97e5dbf
--- /dev/null
+++ b/cypress/integration/ItemPage/ItemZonesInfo.spec.js
@@ -0,0 +1,45 @@
+// Give network requests a bit of breathing room! (A bit more, because this
+// page has a lot of data!)
+const networkTimeout = { timeout: 10000 };
+
+describe("ItemZonesInfo", () => {
+ it("shows simple zone data for an all-species Background", () => {
+ cy.visit("/items/37375");
+ cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
+ "have.text",
+ "Zone: Background"
+ );
+ });
+
+ it("shows simple zone data for a species-specific Hat", () => {
+ cy.visit("/items/34985");
+ cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
+ "have.text",
+ "Zone: Hat"
+ );
+ });
+
+ it("shows distinct zone data for an all-species in-hand item", () => {
+ cy.visit("/items/43397");
+ cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
+ "have.text",
+ "Zones: Right-hand Item (52 species)" + "Left-hand Item (2 species)"
+ );
+
+ cy.contains("(52 species)").focus();
+ cy.contains(
+ "Acara, Aisha, Blumaroo, Bori, Bruce, Buzz, Chia, Chomby, Cybunny, Draik, Elephante, Eyrie, Flotsam, Gelert, Gnorbu, Grarrl, Grundo, Hissi, Ixi, Jetsam, Jubjub, Kacheek, Kau, Kiko, Koi, Korbat, Kougra, Krawk, Kyrii, Lupe, Lutari, Meerca, Moehog, Mynci, Nimmo, Ogrin, Peophin, Poogle, Pteri, Quiggle, Ruki, Scorchio, Shoyru, Skeith, Techo, Tonu, Uni, Usul, Wocky, Xweetok, Yurble, Zafara"
+ ).should("exist");
+
+ cy.contains("(2 species)").focus();
+ cy.contains("Lenny, Tuskaninny").should("exist");
+ });
+
+ it("shows simple zone data for a Mutant-only Dress", () => {
+ cy.visit("/items/70564");
+ cy.get("[data-test-id='item-zones-info']", networkTimeout).should(
+ "have.text",
+ "Zone: Shirt/Dress"
+ );
+ });
+});
diff --git a/src/app/ItemPage.js b/src/app/ItemPage.js
index 769c971..d6af813 100644
--- a/src/app/ItemPage.js
+++ b/src/app/ItemPage.js
@@ -574,9 +574,19 @@ function ItemPageOutfitPreview({ itemId }) {
item(id: $itemId) {
id
name
- compatibleBodies {
- id
- representsAllBodies
+ compatibleBodiesAndTheirZones {
+ body {
+ id
+ representsAllBodies
+ species {
+ id
+ name
+ }
+ }
+ zones {
+ id
+ label @client
+ }
}
canonicalAppearance(
preferredSpeciesId: $preferredSpeciesId
@@ -628,7 +638,10 @@ function ItemPageOutfitPreview({ itemId }) {
}
);
- const compatibleBodies = data?.item?.compatibleBodies || [];
+ const compatibleBodies =
+ data?.item?.compatibleBodiesAndTheirZones?.map(({ body }) => body) || [];
+ const compatibleBodiesAndTheirZones =
+ data?.item?.compatibleBodiesAndTheirZones || [];
// If there's only one compatible body, and the canonical species's name
// appears in the item name, then this is probably a species-specific item,
@@ -825,29 +838,11 @@ function ItemPageOutfitPreview({ itemId }) {
/>
-
+ {compatibleBodiesAndTheirZones.length > 0 && (
+
+ )}
);
@@ -1276,34 +1271,55 @@ function SpeciesFaceOption({
);
}
-function ItemZonesInfo({ zonesAndTheirBodies }) {
- const sortedZonesAndTheirBodies = [...zonesAndTheirBodies].sort((a, b) =>
- zonesAndTheirBodiesSortKey(a).localeCompare(zonesAndTheirBodiesSortKey(b))
+function ItemZonesInfo({ compatibleBodiesAndTheirZones }) {
+ // Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
+ // merging zones with the same label, because that's how user-facing zone UI
+ // generally works!
+ const zoneLabelsAndTheirBodiesMap = {};
+ for (const { body, zones } of compatibleBodiesAndTheirZones) {
+ for (const zone of zones) {
+ if (!zoneLabelsAndTheirBodiesMap[zone.label]) {
+ zoneLabelsAndTheirBodiesMap[zone.label] = {
+ zoneLabel: zone.label,
+ bodies: [],
+ };
+ }
+ zoneLabelsAndTheirBodiesMap[zone.label].bodies.push(body);
+ }
+ }
+ const zoneLabelsAndTheirBodies = Object.values(zoneLabelsAndTheirBodiesMap);
+
+ const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) =>
+ buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare(
+ buildSortKeyForZoneLabelsAndTheirBodies(b)
+ )
);
// We only show body info if there's more than one group of bodies to talk
// about. If they all have the same zones, it's clear from context that any
// preview available in the list has the zones listed here.
const bodyGroups = new Set(
- zonesAndTheirBodies.map(({ bodies }) => bodies.map((b) => b.id).join(","))
+ zoneLabelsAndTheirBodies.map(({ bodies }) =>
+ bodies.map((b) => b.id).join(",")
+ )
);
const showBodyInfo = bodyGroups.size > 1;
return (
-
+
- Zones:
+ {sortedZonesAndTheirBodies.length > 1 ? "Zones" : "Zone"}:
{" "}
- {sortedZonesAndTheirBodies.map(({ zone, bodies }) => (
+ {sortedZonesAndTheirBodies.map(({ zoneLabel, bodies }) => (
@@ -1314,8 +1330,8 @@ function ItemZonesInfo({ zonesAndTheirBodies }) {
);
}
-function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
- let content = zone.label;
+function ItemZonesInfoListItem({ zoneLabel, bodies, showBodyInfo }) {
+ let content = zoneLabel;
if (showBodyInfo) {
if (bodies.some((b) => b.representsAllBodies)) {
@@ -1332,7 +1348,7 @@ function ItemZonesInfoListItem({ zone, bodies, showBodyInfo }) {
content = (
<>
{content}{" "}
-
+
body.representsAllBodies);
- const bodyCountString = bodies.length.toString().padStart(4, "0");
- return `${representsAllBodies ? "A" : "Z"}-${bodyCountString}-${zone.label}`;
+
+ // To sort by body count _descending_, we subtract it from a large number.
+ // Then, to make it work in string comparison, we pad it with leading zeroes.
+ // Hacky but solid!
+ const inverseBodyCount = (9999 - bodies.length).toString().padStart(4, "0");
+
+ console.log(
+ `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`
+ );
+ return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`;
}
/**
diff --git a/src/server/types/Item.js b/src/server/types/Item.js
index 78321ea..635a793 100644
--- a/src/server/types/Item.js
+++ b/src/server/types/Item.js
@@ -85,7 +85,13 @@ const typeDefs = gql`
# All bodies that this item is compatible with. Note that this might return
# the special representsAllPets body, e.g. if this is just a Background!
+ # Deprecated: Impress 2020 now uses compatibleBodiesAndTheirZones.
compatibleBodies: [Body!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
+
+ # All bodies that this item is compatible with, and the zones this item
+ # occupies for that body. Note that this might return the special
+ # representsAllPets body, e.g. if this is just a Background!
+ compatibleBodiesAndTheirZones: [BodyAndZones!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
}
type ItemAppearance {
@@ -97,6 +103,11 @@ const typeDefs = gql`
restrictedZones: [Zone!]!
}
+ type BodyAndZones {
+ body: Body!
+ zones: [Zone!]!
+ }
+
input FitsPetSearchFilter {
speciesId: ID!
colorId: ID!
@@ -404,6 +415,30 @@ const resolvers = {
const bodies = bodyIds.map((id) => ({ id }));
return bodies;
},
+ compatibleBodiesAndTheirZones: async ({ id }, _, { db }) => {
+ const [rows, __] = await db.query(
+ `
+ SELECT
+ swf_assets.body_id AS bodyId,
+ (SELECT species_id FROM pet_types WHERE body_id = bodyId LIMIT 1)
+ AS speciesId,
+ GROUP_CONCAT(DISTINCT swf_assets.zone_id) AS zoneIds
+ FROM items
+ INNER JOIN parents_swf_assets ON
+ items.id = parents_swf_assets.parent_id AND
+ parents_swf_assets.parent_type = "Item"
+ INNER JOIN swf_assets ON
+ parents_swf_assets.swf_asset_id = swf_assets.id
+ WHERE items.id = ?
+ GROUP BY swf_assets.body_id
+ `,
+ [id]
+ );
+ return rows.map((row) => ({
+ body: { id: row.bodyId, species: { id: row.speciesId } },
+ zones: row.zoneIds.split(",").map((zoneId) => ({ id: zoneId })),
+ }));
+ },
},
ItemAppearance: {