useInventoryBadges.ts 8.2 Ko
Télécharger le fichier
import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@octane/renderer';
import { useEffect, useRef, useState } from 'react';
import { registerSharedHook, useSharedHook } from '@/state/useSharedHook';
import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api';
import { rememberBadgeRarityFromPacket, rememberReceivedBadgeRarity } from '../../api/badges/badgeRarity';
import { useMessageEvent } from '../events';
import { createTrailingSender } from './badgeWearSender';
import { useSharedVisibility } from '../useSharedVisibility';
import { useInventoryUnseenTracker } from './useInventoryUnseenTracker';
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 | null)[]>([]);
const activeBadgeCodesRef = useRef<(string | null)[]>([]);
activeBadgeCodesRef.current = activeBadgeCodes;
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 isWearingBadge = (badgeCode: string) => activeBadgeCodes.some((code) => code === badgeCode);
const canWearBadges = () => activeBadgeCodes.filter(Boolean).length < maxBadgeCount;
const toFixedSlots = (arr: (string | null)[]): (string | null)[] => {
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)[]) => {
const composer = new SetActivatedBadgesComposer();
for (let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
SendMessageComposer(composer);
};
const sendActiveBadgesRef = useRef(sendActiveBadges);
sendActiveBadgesRef.current = sendActiveBadges;
const [wearSender] = useState(() => createTrailingSender<(string | null)[]>((slots) => sendActiveBadgesRef.current(slots)));
// A change still waiting is sent when the hook goes away, not dropped.
useEffect(() => () => wearSender.flush(), [wearSender]);
/**
* Keeps a new set of worn badges at once and sends the last of a quick run of changes. Every change is worked out
* from a ref, not inside a state updater: an updater may run twice (StrictMode) and sent the set twice.
*/
const wearBadges = (slots: (string | null)[]) => {
activeBadgeCodesRef.current = slots;
setActiveBadgeCodes(slots);
wearSender.push(slots);
};
const toggleBadge = (badgeCode: string) => {
const slots = toFixedSlots(activeBadgeCodesRef.current);
const index = slots.indexOf(badgeCode);
if (index === -1) {
const emptySlot = slots.indexOf(null);
if (emptySlot === -1) return;
slots[emptySlot] = badgeCode;
} else {
slots[index] = null;
}
wearBadges(slots);
};
const getBadgeId = (badgeCode: string) => {
const index = badgeCodes.indexOf(badgeCode);
if (index === -1) return 0;
return badgeIds.get(badgeCode) ?? 0;
};
useMessageEvent<BadgesEvent>(BadgesEvent, (event) => {
const parser = event.getParser();
const allBadgeCodes = parser.getAllBadgeCodes();
// Official `BadgesModel.initBadges`: the packet itself carries the owner count and rarity
// tier of every badge, so the grid never has to ask the leaderboard for them.
rememberBadgeRarityFromPacket(parser.getBadgeDetails());
setBadgeIds(() => {
const newValue = new Map<string, number>();
allBadgeCodes.forEach((code) => {
const badgeId = parser.getBadgeId(code);
newValue.set(code, badgeId);
});
return newValue;
});
// The emulator answers SetActivatedBadges (UserWearBadgeEvent) with a
// UserBadgesComposer room broadcast, NOT a BadgesEvent — so there is no
// echo to suppress and the old pendingUpdatesRef counter only ever
// leaked (incremented on every edit, never decremented), which then
// silently dropped legitimate later BadgesEvent updates. The server is
// authoritative here (edits are already persisted), so always apply it.
// A set the user changed and is not sent yet wins over this older one.
if (!wearSender.isPending()) setActiveBadgeCodes(toFixedSlots(parser.getActiveBadgeCodes()));
setBadgeCodes(allBadgeCodes);
});
useMessageEvent<BadgeReceivedEvent>(BadgeReceivedEvent, (event) => {
const parser = event.getParser();
rememberReceivedBadgeRarity(parser);
const unseen = isUnseen(UnseenItemCategory.BADGE, parser.badgeId);
setBadgeCodes((prevValue) => {
const newValue = [...prevValue];
if (unseen) newValue.unshift(parser.badgeCode);
else newValue.push(parser.badgeCode);
return newValue;
});
setBadgeIds((prevValue) => {
const newValue = new Map(prevValue);
newValue.set(parser.badgeCode, parser.badgeId);
return newValue;
});
});
useEffect(() => {
if (!badgeCodes || !badgeCodes.length) return;
setSelectedBadgeCode((prevValue) => {
let newValue = prevValue;
if (newValue && badgeCodes.indexOf(newValue) === -1) newValue = null;
if (!newValue) newValue = badgeCodes[0];
return newValue;
});
}, [badgeCodes]);
useEffect(() => {
if (!isVisible) return;
return () => {
resetCategory(UnseenItemCategory.BADGE);
};
}, [isVisible, resetCategory]);
useEffect(() => {
if (!isVisible || !needsUpdate) return;
SendMessageComposer(new RequestBadgesComposer());
setNeedsUpdate(false);
}, [isVisible, needsUpdate]);
const setBadgeAtSlot = (badgeCode: string, slotIndex: number) => {
const slots = toFixedSlots(activeBadgeCodesRef.current);
// Remove badge if already in another slot
const existingIndex = slots.indexOf(badgeCode);
if (existingIndex >= 0) slots[existingIndex] = null;
// Place badge at target slot
slots[slotIndex] = badgeCode;
wearBadges(slots);
};
const removeBadge = (badgeCode: string) => {
const slots = toFixedSlots(activeBadgeCodesRef.current);
const index = slots.indexOf(badgeCode);
if (index === -1) return;
slots[index] = null;
wearBadges(slots);
};
const reorderBadges = (fromIndex: number, toIndex: number) => {
if (fromIndex === toIndex) return;
const slots = toFixedSlots(activeBadgeCodesRef.current);
const temp = slots[fromIndex];
slots[fromIndex] = slots[toIndex];
slots[toIndex] = temp;
wearBadges(slots);
};
const swapBadges = (fromIndex: number, toIndex: number) => {
if (fromIndex === toIndex) return;
const slots = toFixedSlots(activeBadgeCodesRef.current);
const temp = slots[fromIndex];
slots[fromIndex] = slots[toIndex];
slots[toIndex] = temp;
wearBadges(slots);
};
const requestBadges = () => {
SendMessageComposer(new RequestBadgesComposer());
};
return {
badgeCodes,
activeBadgeCodes,
selectedBadgeCode,
setSelectedBadgeCode,
isWearingBadge,
canWearBadges,
toggleBadge,
getBadgeId,
setBadgeAtSlot,
removeBadge,
reorderBadges,
swapBadges,
requestBadges,
activate,
deactivate
};
};
export const useInventoryBadges = () => useSharedHook(useInventoryBadgesState);
registerSharedHook(useInventoryBadgesState);
