Badge DnD rework: fix duplicate/disappearing badges, add visual feedback

Fix slot 0 drag bug ('0' is falsy), prevent badge duplication from stale
props fallback in InfoStand, add sparse slot support, fix race condition
with pending server updates. Add drag preview, glow animations, drop
settle effect, and remove-badge indicator overlay.
This commit is contained in:
Life
2026-04-04 13:34:53 +02:00
parent 203a754ce1
commit 73ee9c7603
6 changed files with 173 additions and 67 deletions
+50 -44
View File
@@ -11,19 +11,31 @@ const useInventoryBadgesState = () =>
const [ needsUpdate, setNeedsUpdate ] = useState(true);
const [ badgeCodes, setBadgeCodes ] = useState<string[]>([]);
const [ badgeIds, setBadgeIds ] = useState<Map<string, number>>(new Map<string, number>());
const [ activeBadgeCodes, setActiveBadgeCodes ] = useState<string[]>([]);
const [ activeBadgeCodes, setActiveBadgeCodes ] = useState<(string | null)[]>([]);
const [ selectedBadgeCode, setSelectedBadgeCode ] = useState<string>(null);
const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility();
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
const maxBadgeCount = GetConfigurationValue<number>('user.badges.max.slots', 5);
const localChangeRef = useRef(false);
const isWearingBadge = (badgeCode: string) => (activeBadgeCodes.indexOf(badgeCode) >= 0);
const canWearBadges = () => (activeBadgeCodes.length < maxBadgeCount);
const pendingUpdatesRef = useRef(0);
const isWearingBadge = (badgeCode: string) => activeBadgeCodes.some(code => code === badgeCode);
const canWearBadges = () => (activeBadgeCodes.filter(Boolean).length < maxBadgeCount);
const sendActiveBadges = (badges: string[]) =>
const toFixedSlots = (arr: (string | null)[]): (string | null)[] =>
{
localChangeRef.current = true;
const seen = new Set<string>();
return Array.from({ length: maxBadgeCount }, (_, i) =>
{
const code = arr[i] || null;
if(!code || seen.has(code)) return null;
seen.add(code);
return code;
});
};
const sendActiveBadges = (badges: (string | null)[]) =>
{
pendingUpdatesRef.current++;
const composer = new SetActivatedBadgesComposer();
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
SendMessageComposer(composer);
@@ -33,24 +45,23 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
const newValue = [ ...prevValue ];
const index = newValue.indexOf(badgeCode);
const slots = toFixedSlots(prevValue);
const index = slots.indexOf(badgeCode);
if(index === -1)
{
if(newValue.length >= maxBadgeCount) return prevValue;
const emptySlot = slots.indexOf(null);
if(emptySlot === -1) return prevValue;
newValue.push(badgeCode);
slots[emptySlot] = badgeCode;
}
else
{
newValue.splice(index, 1);
slots[index] = null;
}
sendActiveBadges(newValue);
return newValue;
sendActiveBadges(slots);
return slots;
});
};
@@ -82,14 +93,15 @@ const useInventoryBadgesState = () =>
return newValue;
});
// Skip overwriting activeBadgeCodes if we recently made a local change
if(localChangeRef.current)
// Skip overwriting activeBadgeCodes if we have pending local changes
if(pendingUpdatesRef.current > 0)
{
localChangeRef.current = false;
pendingUpdatesRef.current--;
}
else
{
setActiveBadgeCodes(parser.getActiveBadgeCodes());
const serverBadges = parser.getActiveBadgeCodes();
setActiveBadgeCodes(toFixedSlots(serverBadges));
}
setBadgeCodes(allBadgeCodes);
@@ -159,8 +171,7 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
// Build a fixed-size array of maxBadgeCount slots
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
const slots = toFixedSlots(prevValue);
// Remove badge if already in another slot
const existingIndex = slots.indexOf(badgeCode);
@@ -169,11 +180,8 @@ const useInventoryBadgesState = () =>
// Place badge at target slot
slots[slotIndex] = badgeCode;
// Compact: remove nulls, keep order
const result = slots.filter(Boolean) as string[];
sendActiveBadges(result);
return result;
sendActiveBadges(slots);
return slots;
});
};
@@ -181,10 +189,14 @@ const useInventoryBadgesState = () =>
{
setActiveBadgeCodes(prevValue =>
{
const result = prevValue.filter(code => code !== badgeCode);
const slots = toFixedSlots(prevValue);
const index = slots.indexOf(badgeCode);
if(index === -1) return prevValue;
sendActiveBadges(result);
return result;
slots[index] = null;
sendActiveBadges(slots);
return slots;
});
};
@@ -193,14 +205,14 @@ const useInventoryBadgesState = () =>
setActiveBadgeCodes(prevValue =>
{
if(fromIndex === toIndex) return prevValue;
if(fromIndex >= prevValue.length) return prevValue;
const newValue = [ ...prevValue ];
const [ moved ] = newValue.splice(fromIndex, 1);
newValue.splice(toIndex, 0, moved);
const slots = toFixedSlots(prevValue);
const temp = slots[fromIndex];
slots[fromIndex] = slots[toIndex];
slots[toIndex] = temp;
sendActiveBadges(newValue);
return newValue;
sendActiveBadges(slots);
return slots;
});
};
@@ -210,19 +222,13 @@ const useInventoryBadgesState = () =>
{
if(fromIndex === toIndex) return prevValue;
// Build fixed-size array so swap works even with empty slots
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
// Swap the two slots
const slots = toFixedSlots(prevValue);
const temp = slots[fromIndex];
slots[fromIndex] = slots[toIndex];
slots[toIndex] = temp;
// Compact: remove nulls, keep order
const result = slots.filter(Boolean) as string[];
sendActiveBadges(result);
return result;
sendActiveBadges(slots);
return slots;
});
};