import gql from "graphql-tag"; function getVisibleLayers(petAppearance, itemAppearances) { if (!petAppearance) { return []; } const validItemAppearances = itemAppearances.filter((a) => a); const petLayers = petAppearance.layers.map((l) => ({ ...l, source: "pet" })); const itemLayers = validItemAppearances .map((a) => a.layers) .flat() .map((l) => ({ ...l, source: "item" })); let allLayers = [...petLayers, ...itemLayers]; const itemRestrictedZoneIds = new Set( validItemAppearances .map((a) => a.restrictedZones) .flat() .map((z) => z.id), ); const petRestrictedZoneIds = new Set( petAppearance.restrictedZones.map((z) => z.id), ); const visibleLayers = allLayers.filter((layer) => { // When an item restricts a zone, it hides pet layers of the same zone. // We use this to e.g. make a hat hide a hair ruff. // // NOTE: Items' restricted layers also affect what items you can wear at // the same time. We don't enforce anything about that here, and // instead assume that the input by this point is valid! if (layer.source === "pet" && itemRestrictedZoneIds.has(layer.zone.id)) { return false; } // When a pet appearance restricts a zone, or when the pet is Unconverted, // it makes body-specific items incompatible. We use this to disallow UCs // from wearing certain body-specific Biology Effects, Statics, etc, while // still allowing non-body-specific items in those zones! (I think this // happens for some Invisible pet stuff, too?) // // TODO: We shouldn't be *hiding* these zones, like we do with items; we // should be doing this way earlier, to prevent the item from even // showing up even in search results! // // NOTE: This can result in both pet layers and items occupying the same // zone, like Static, so long as the item isn't body-specific! That's // correct, and the item layer should be on top! (Here, we implement // it by placing item layers second in the list, and rely on JS sort // stability, and *then* rely on the UI to respect that ordering when // rendering them by depth. Not great! 😅) // // NOTE: We used to also include the pet appearance's *occupied* zones in // this condition, not just the restricted zones, as a sensible // defensive default, even though we weren't aware of any relevant // items. But now we know that actually the "Bruce Brucey B Mouth" // occupies the real Mouth zone, and still should be visible and // above pet layers! So, we now only check *restricted* zones. // // NOTE: UCs used to implement their restrictions by listing specific // zones, but it seems that the logic has changed to just be about // UC-ness and body-specific-ness, and not necessarily involve the // set of restricted zones at all. (This matters because e.g. UCs // shouldn't show _any_ part of the Rainy Day Umbrella, but most UCs // don't restrict Right-Hand Item (Zone 49).) Still, I'm keeping the // zone restriction case running too, because I don't think it // _hurts_ anything, and I'm not confident enough in this conclusion. // // TODO: Do Invisibles follow this new rule like UCs, too? Or do they still // use zone restrictions? if ( layer.source === "item" && layer.bodyId !== "0" && (petAppearance.pose === "UNCONVERTED" || petRestrictedZoneIds.has(layer.zone.id)) ) { return false; } // A pet appearance can also restrict its own zones. The Wraith Uni is an // interesting example: it has a horn, but its zone restrictions hide it! if (layer.source === "pet" && petRestrictedZoneIds.has(layer.zone.id)) { return false; } return true; }); visibleLayers.sort((a, b) => a.zone.depth - b.zone.depth); return visibleLayers; } export const itemAppearanceFragmentForGetVisibleLayers = gql` fragment ItemAppearanceForGetVisibleLayers on ItemAppearance { id layers { id bodyId zone { id depth } } restrictedZones { id } } `; export const petAppearanceFragmentForGetVisibleLayers = gql` fragment PetAppearanceForGetVisibleLayers on PetAppearance { id pose layers { id zone { id depth } } restrictedZones { id } } `; export default getVisibleLayers;