InventoryView.tsx 15.4 Ko
Télécharger le fichier
import {
AddLinkEventTracker,
BadgePointLimitsEvent,
GetLocalizationManager,
GetRoomEngine,
ILinkEventTracker,
IRoomSession,
RemoveLinkEventTracker,
RoomEngineObjectEvent,
RoomEngineObjectPlacedEvent,
RoomPreviewer,
RoomSessionEvent
} from '@octane/renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import {
ensureBadgeLeaderboardLoaded,
filterFurnitureGroupItems,
FURNI_MAIN_FILTER,
FurniMainFilter,
FurnitureSection,
inFurnitureSection,
getCachedBadgeRarityStat,
isObjectMoverRequested,
LocalizeBadgeName,
LocalizeText,
setObjectMoverRequested,
UnseenItemCategory
} from '../../api';
import { OctaneCardHeaderView, OctaneCardTabsItemView, OctaneCardTabsView, OctaneCardView } from '../../common';
import { isCollectiblesEnabled } from '../../hooks/collectibles/useCollectibles';
import {
useInventoryBadges,
useInventoryFurni,
useInventoryTrade,
useWiredTrading,
useInventoryUnseenTracker,
useMessageEvent,
useOctaneEvent
} from '../../hooks';
import { InventoryBadgeView } from './views/badge/InventoryBadgeView';
import { InventoryBotView } from './views/bot/InventoryBotView';
import { InventoryCollectiblesView } from './views/collectibles/InventoryCollectiblesView';
import { InventoryFurnitureDeleteView } from './views/furniture/InventoryFurnitureDeleteView';
import { InventoryTradeNameScamWarningView } from './views/furniture/InventoryTradeNameScamWarningView';
import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView';
import { InventoryTradeView } from './views/furniture/InventoryTradeView';
import { InventoryWiredTradeView } from './views/furniture/InventoryWiredTradeView';
import { BADGE_MAIN_ACHIEVEMENTS, BADGE_MAIN_ALL, BADGE_MAIN_NORMAL, BADGE_RARITY_ALL, InventoryCategoryFilterView } from './views/InventoryCategoryFilterView';
import { InventoryPetView } from './views/pet/InventoryPetView';
import { InventoryPrefixView } from './views/prefix/InventoryPrefixView';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
const TAB_FURNITURE = 'inventory.furni';
const TAB_BOTS = 'inventory.bots';
const TAB_PETS = 'inventory.furni.tab.pets';
const TAB_BADGES = 'inventory.badges';
const TAB_PREFIXES = 'inventory.prefixes';
const TAB_COLLECTIBLES = 'inventory.collectibles';
const TAB_RENTABLES = 'inventory.rentables';
// The order of the official inventory, with the prefixes of this client last.
const TABS = [TAB_FURNITURE, TAB_COLLECTIBLES, TAB_RENTABLES, TAB_PETS, TAB_BADGES, TAB_BOTS, TAB_PREFIXES];
const SECTION_BY_TAB: Record<string, FurnitureSection> = {
[TAB_FURNITURE]: 'furni',
[TAB_COLLECTIBLES]: 'collectibles',
[TAB_RENTABLES]: 'rentables'
};
const TAB_LABEL_FALLBACK: Record<string, string> = {
[TAB_FURNITURE]: 'Furniture',
[TAB_COLLECTIBLES]: 'Collectibles',
[TAB_RENTABLES]: 'Rentables',
[TAB_PETS]: 'Pets',
[TAB_BADGES]: 'Badges',
[TAB_BOTS]: 'Bots',
[TAB_PREFIXES]: 'Prefixes'
};
const tabLabel = (name: string) => {
const value = LocalizeText(name);
if (name === TAB_BADGES && value === 'Achieved badges') return 'Badges';
return value && value !== name ? value : TAB_LABEL_FALLBACK[name] || name;
};
const TAB_BY_CODE: Record<string, string> = {
furni: TAB_FURNITURE,
furniture: TAB_FURNITURE,
collectibles: TAB_COLLECTIBLES,
rentables: TAB_RENTABLES,
pets: TAB_PETS,
badges: TAB_BADGES,
prefixes: TAB_PREFIXES,
bots: TAB_BOTS
};
const UNSEEN_BY_TAB: Record<string, number> = {
[TAB_FURNITURE]: UnseenItemCategory.FURNI,
[TAB_RENTABLES]: UnseenItemCategory.RENTABLE,
[TAB_PETS]: UnseenItemCategory.PET,
[TAB_BADGES]: UnseenItemCategory.BADGE,
[TAB_BOTS]: UnseenItemCategory.BOT,
[TAB_PREFIXES]: UnseenItemCategory.PREFIX
};
const getTabUnseenCount = (name: string, getCount: (category: number) => number) => {
const category = UNSEEN_BY_TAB[name];
return category === undefined ? 0 : getCount(category);
};
const RARITY_TO_ID: Record<string, number> = {
common: 0,
uncommon: 1,
rare: 2,
epic: 3,
mythical: 4,
legendary: 5,
unique: 6
};
export const InventoryView: FC<{}> = () => {
const [isVisible, setIsVisible] = useState(false);
const [currentTab, setCurrentTab] = useState<string>(TABS[0]);
const [roomSession, setRoomSession] = useState<IRoomSession>(null);
const [roomPreviewer, setRoomPreviewer] = useState<RoomPreviewer>(null);
const [searchValue, setSearchValue] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [badgeMetadata, setBadgeMetadata] = useState<Awaited<ReturnType<typeof ensureBadgeLeaderboardLoaded>>>(null);
const [mainFilter, setMainFilter] = useState<string>(FURNI_MAIN_FILTER.ALL);
const [typeFilter, setTypeFilter] = useState<string>('any');
const { isTrading = false, stopTrading = null } = useInventoryTrade();
const { isOpen: isWiredTrading = false, cancel: cancelWiredTrade = null } = useWiredTrading();
const { getCount = null } = useInventoryUnseenTracker();
const { groupItems = [] } = useInventoryFurni();
const { badgeCodes = [] } = useInventoryBadges();
useEffect(() => {
setSearchValue('');
setAppliedSearch('');
if (currentTab === TAB_BADGES) {
setMainFilter(BADGE_MAIN_ALL);
setTypeFilter(String(BADGE_RARITY_ALL));
ensureBadgeLeaderboardLoaded()
.then(setBadgeMetadata)
.catch(() => setBadgeMetadata(null));
} else {
setMainFilter(FURNI_MAIN_FILTER.ALL);
setTypeFilter('any');
}
}, [currentTab]);
// With collectibles on, their tab holds the owned collectibles and numbered furni stay with the furni.
const collectiblesEnabled = isCollectiblesEnabled();
const showsCollectibles = collectiblesEnabled && currentTab === TAB_COLLECTIBLES;
const section = showsCollectibles ? null : (SECTION_BY_TAB[currentTab] ?? null);
useEffect(() => {
if (!section) return;
setTypeFilter('any');
}, [mainFilter, section]);
// The items of the open tab (furni, collectibles or rentables): its empty view and filter bar go by these.
const sectionGroupItems = useMemo(() => (section ? inFurnitureSection(groupItems, section, collectiblesEnabled) : groupItems), [groupItems, section, collectiblesEnabled]);
const filteredGroupItems = useMemo(() => {
if (!section) return groupItems;
return filterFurnitureGroupItems(sectionGroupItems, appliedSearch, mainFilter as FurniMainFilter, typeFilter);
}, [groupItems, sectionGroupItems, appliedSearch, mainFilter, typeFilter, section]);
const filteredBadgeCodes = useMemo(() => {
const comparison = appliedSearch.toLocaleLowerCase().trim();
const rarityFilter = Number(typeFilter);
const achievementBadges = badgeCodes.filter((badge) => badge.startsWith('ACH_'));
const numberMap: { [key: string]: number } = {};
achievementBadges.forEach((badge) => {
const name = badge.split(/[\d]+/)[0];
const number = Number(badge.replace(name, ''));
if (numberMap[name] === undefined || number > numberMap[name]) numberMap[name] = number;
});
let deduped = Object.keys(numberMap)
.map((name) => `${name}${numberMap[name]}`)
.concat(badgeCodes.filter((badge) => !badge.startsWith('ACH_')));
if (mainFilter === BADGE_MAIN_NORMAL) deduped = deduped.filter((code) => !code.startsWith('ACH_'));
if (mainFilter === BADGE_MAIN_ACHIEVEMENTS) deduped = deduped.filter((code) => code.startsWith('ACH_'));
return deduped.filter((badgeCode) => {
if (!LocalizeBadgeName(badgeCode).toLocaleLowerCase().includes(comparison)) return false;
if (rarityFilter === BADGE_RARITY_ALL) return true;
const stat = badgeMetadata ? getCachedBadgeRarityStat(badgeCode) : null;
if (!stat) return rarityFilter === 0;
return (RARITY_TO_ID[stat.rarity] ?? -99) === rarityFilter;
});
}, [badgeCodes, appliedSearch, mainFilter, typeFilter, badgeMetadata]);
// AIR WiredTradingModel.closingInventoryView: closing the inventory ends a wired trade (the offer goes back); left
// open, the window would only pop up again while the trade runs.
const onClose = () => {
if (isTrading) stopTrading();
else if (isWiredTrading && cancelWiredTrade) cancelWiredTrade();
setIsVisible(false);
};
useOctaneEvent<RoomEngineObjectPlacedEvent>(RoomEngineObjectEvent.PLACED, (event) => {
if (!isObjectMoverRequested()) return;
setObjectMoverRequested(false);
if (!event.placedInRoom) setIsVisible(true);
});
useOctaneEvent<RoomSessionEvent>([RoomSessionEvent.CREATED, RoomSessionEvent.ENDED], (event) => {
switch (event.type) {
case RoomSessionEvent.CREATED:
setRoomSession(event.session);
return;
case RoomSessionEvent.ENDED:
setRoomSession(null);
setIsVisible(false);
return;
}
});
useMessageEvent<BadgePointLimitsEvent>(BadgePointLimitsEvent, (event) => {
const parser = event.getParser();
for (const data of parser.data) GetLocalizationManager().setBadgePointLimit(data.badgeId, data.limit);
});
useEffect(() => {
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => {
const parts = url.split('/');
if (parts.length < 2) return;
switch (parts[1]) {
case 'show':
setIsVisible(true);
if (parts[2] && TAB_BY_CODE[parts[2]]) setCurrentTab(TAB_BY_CODE[parts[2]]);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible((prevValue) => !prevValue);
if (parts[2] && TAB_BY_CODE[parts[2]]) setCurrentTab(TAB_BY_CODE[parts[2]]);
return;
}
},
eventUrlPrefix: 'inventory/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
useEffect(() => {
const previewer = new RoomPreviewer(GetRoomEngine(), ++RoomPreviewer.PREVIEW_COUNTER);
previewer.backgroundColor = null;
setRoomPreviewer(previewer);
return () => {
previewer.dispose();
setRoomPreviewer(null);
};
}, []);
useEffect(() => {
if (!isVisible && (isTrading || isWiredTrading)) setIsVisible(true);
}, [isVisible, isTrading, isWiredTrading]);
if (!isVisible) return null;
const showFilter = !isTrading && !isWiredTrading && ((section !== null && sectionGroupItems.length > 0) || currentTab === TAB_BADGES);
return (
<NativeTextTree>
<>
<OctaneCardView
className={`octane-inventory-window max-w-[calc(100vw-16px)] ${!isTrading && isWiredTrading ? 'is-wired-trading' : currentTab === TAB_BADGES ? 'has-badge-controls' : currentTab === TAB_PETS ? 'has-pet-controls' : ''}`}
frameStyle={3}
resizeAxis="vertical"
uniqueKey="inventory"
>
<OctaneCardHeaderView headerText={LocalizeText('inventory.title')} onCloseClick={onClose} />
{!isTrading && !isWiredTrading && (
<>
<OctaneCardTabsView classNames={['octane-inventory-tabs-shell']}>
{TABS.map((name) => (
<OctaneCardTabsItemView
key={name}
count={getTabUnseenCount(name, getCount)}
isActive={currentTab === name}
onClick={() => setCurrentTab(name)}
>
<span className="octane-inventory-tab-label">{tabLabel(name)}</span>
</OctaneCardTabsItemView>
))}
</OctaneCardTabsView>
<div className="octane-inventory-body">
{showFilter && (
<InventoryCategoryFilterView
currentTab={currentTab}
mainFilter={mainFilter}
typeFilter={typeFilter}
searchValue={searchValue}
onMainFilterChange={(value) => {
setMainFilter(value);
setAppliedSearch(searchValue);
}}
onTypeFilterChange={(value) => {
setTypeFilter(value);
setAppliedSearch(searchValue);
}}
onSearchChange={setSearchValue}
onSearchApply={setAppliedSearch}
/>
)}
<div className={`octane-inventory-content ${section ? 'is-furniture' : ''}`}>
{section && (
<InventoryFurnitureView
filteredGroupItems={filteredGroupItems}
roomPreviewer={roomPreviewer}
roomSession={roomSession}
sectionGroupItems={sectionGroupItems}
/>
)}
{showsCollectibles && <InventoryCollectiblesView />}
{currentTab === TAB_PETS && <InventoryPetView roomPreviewer={roomPreviewer} roomSession={roomSession} />}
{currentTab === TAB_BADGES && <InventoryBadgeView filteredBadgeCodes={filteredBadgeCodes} />}
{currentTab === TAB_BOTS && <InventoryBotView roomPreviewer={roomPreviewer} roomSession={roomSession} />}
{currentTab === TAB_PREFIXES && <InventoryPrefixView />}
</div>
</div>
</>
)}
{isTrading && (
<div className="octane-inventory-body is-trade">
<InventoryTradeView cancelTrade={onClose} />
</div>
)}
{!isTrading && isWiredTrading && (
<div className="octane-inventory-body is-trade">
<InventoryWiredTradeView />
</div>
)}
</OctaneCardView>
<InventoryFurnitureDeleteView />
<InventoryTradeNameScamWarningView />
</>
</NativeTextTree>
);
};
