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:
Emi Matchu 2024-02-01 03:14:00 -08:00
parent cb90b79efe
commit 578528f468
2 changed files with 48 additions and 16 deletions

View file

@ -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

View file

@ -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;
}