Better handling for items in different zones with the same name
Specifically, I was looking at the new "Stormy Cloud Kacheek" items, and was surprised to find that, in the outfit editor, they all get grouped under "Markings" (and therefore the UI treats them as mutually-exclusive via hidden radio button and only bolds one at a time), but they aren't actually conflicting because they occupy different zones named "Markings". In this change, we make the zone groups actually just be *by zone* rather than jumbling all of the zones with the same label together; but in most cases, we still keep the same simplified display. In the case of the "Stormy Cloud Kacheek" items though, we now get a few groups: `Glasses`, `Markings (#6)`, and `Markings (#16)`. Glasses is chosen by coincidence because it's the first zone label for that item alphabetically (even though that item also occupies a third "Markings" zone), and then the other two know to disambiguate from each other. There's an opportunity here to cheat things further, like to *intentionally* select items like "Glasses" that are less ambiguous when possible. I'm not aware of enough other cases like this for that to really matter, though, so I'm just leaving it as-is! I tested this a *bit* on other outfits, and everything looked fine at a glance, so I'm just moving forward—but I'll make an announcement to ask people to help take a look!
This commit is contained in:
parent
cb90b79efe
commit
578528f468
2 changed files with 48 additions and 16 deletions
|
@ -78,9 +78,9 @@ function ItemsPanel({ outfitState, outfitSaving, loading, dispatchToOutfit }) {
|
|||
/>
|
||||
) : (
|
||||
<TransitionGroup component={null}>
|
||||
{zonesAndItems.map(({ zoneLabel, items }) => (
|
||||
{zonesAndItems.map(({ zoneId, zoneLabel, items }) => (
|
||||
<CSSTransition
|
||||
key={zoneLabel}
|
||||
key={zoneId}
|
||||
{...fadeOutAndRollUpTransition(css)}
|
||||
>
|
||||
<ItemZoneGroup
|
||||
|
|
|
@ -560,36 +560,49 @@ function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
|||
.map((id) => itemsById[id])
|
||||
.filter((i) => i);
|
||||
|
||||
// We use zone label here, rather than ID, because some zones have the same
|
||||
// label and we *want* to over-simplify that in this UI. (e.g. there are
|
||||
// multiple Hat zones, and some items occupy different ones, but mostly let's
|
||||
// just group them and if they don't conflict then all the better!)
|
||||
// Loop over all the items, grouping them by zone, and also gathering all the
|
||||
// zone metadata.
|
||||
const allItems = [...wornItems, ...closetedItems];
|
||||
const itemsByZoneLabel = new Map();
|
||||
const itemsByZone = new Map();
|
||||
const zonesById = new Map();
|
||||
for (const item of allItems) {
|
||||
if (!item.appearanceOn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const layer of item.appearanceOn.layers) {
|
||||
const zoneLabel = layer.zone.label;
|
||||
const zoneId = layer.zone.id;
|
||||
zonesById.set(zoneId, layer.zone);
|
||||
|
||||
if (!itemsByZoneLabel.has(zoneLabel)) {
|
||||
itemsByZoneLabel.set(zoneLabel, []);
|
||||
if (!itemsByZone.has(zoneId)) {
|
||||
itemsByZone.set(zoneId, []);
|
||||
}
|
||||
itemsByZoneLabel.get(zoneLabel).push(item);
|
||||
itemsByZone.get(zoneId).push(item);
|
||||
}
|
||||
}
|
||||
|
||||
let zonesAndItems = Array.from(itemsByZoneLabel.entries()).map(
|
||||
([zoneLabel, items]) => ({
|
||||
zoneLabel: zoneLabel,
|
||||
// Convert `itemsByZone` into an array of item groups.
|
||||
let zonesAndItems = Array.from(itemsByZone.entries()).map(
|
||||
([zoneId, items]) => ({
|
||||
zoneId,
|
||||
zoneLabel: zonesById.get(zoneId).label,
|
||||
items: [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}),
|
||||
);
|
||||
zonesAndItems.sort((a, b) => a.zoneLabel.localeCompare(b.zoneLabel));
|
||||
|
||||
// As one last step, try to remove zone groups that aren't helpful.
|
||||
// Sort groups by the zone label's alphabetically, and tiebreak by the zone
|
||||
// ID. (That way, "Markings (#6)" sorts before "Markings (#16)".) We do this
|
||||
// before the data simplification step, because it's useful to have
|
||||
// consistent ordering for the algorithm that might choose to skip zones!
|
||||
zonesAndItems.sort((a, b) => {
|
||||
if (a.zoneLabel !== b.zoneLabel) {
|
||||
return a.zoneLabel.localeCompare(b.zoneLabel);
|
||||
} else {
|
||||
return a.zoneId - b.zoneId;
|
||||
}
|
||||
});
|
||||
|
||||
// Data simplification step! Try to remove zone groups that aren't helpful.
|
||||
const groupsWithConflicts = zonesAndItems.filter(
|
||||
({ items }) => items.length > 1,
|
||||
);
|
||||
|
@ -620,6 +633,25 @@ function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
|||
}
|
||||
});
|
||||
|
||||
// Finally, for groups with the same label, append the ID number.
|
||||
//
|
||||
// First, loop over the groups, to count how many times each zone label is
|
||||
// used. Then, loop over them again, appending the ID number if count > 1.
|
||||
const labelCounts = new Map();
|
||||
for (const itemZoneGroup of zonesAndItems) {
|
||||
const { zoneId, zoneLabel } = itemZoneGroup;
|
||||
|
||||
const count = labelCounts.get(zoneLabel) ?? 0;
|
||||
labelCounts.set(zoneLabel, count + 1);
|
||||
}
|
||||
for (const itemZoneGroup of zonesAndItems) {
|
||||
const { zoneId, zoneLabel } = itemZoneGroup;
|
||||
|
||||
if (labelCounts.get(zoneLabel) > 1) {
|
||||
itemZoneGroup.zoneLabel += ` (#${zoneId})`;
|
||||
}
|
||||
}
|
||||
|
||||
return zonesAndItems;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue